remove web

This commit is contained in:
Jan Prochazka
2021-02-20 19:15:11 +01:00
parent dd7db5904c
commit daf9e9d18b
240 changed files with 0 additions and 22572 deletions

View File

@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,57 +0,0 @@
import React from 'react';
import './index.css';
import Screen from './Screen';
import {
CurrentWidgetProvider,
CurrentDatabaseProvider,
OpenedTabsProvider,
OpenedConnectionsProvider,
LeftPanelWidthProvider,
CurrentArchiveProvider,
CurrentThemeProvider,
} from './utility/globalState';
import { SocketProvider } from './utility/SocketProvider';
import ConnectionsPinger from './utility/ConnectionsPinger';
import { ModalLayerProvider } from './modals/showModal';
import UploadsProvider from './utility/UploadsProvider';
import ThemeHelmet from './themes/ThemeHelmet';
import PluginsProvider from './plugins/PluginsProvider';
import { ExtensionsProvider } from './utility/useExtensions';
import { MenuLayerProvider } from './modals/showMenu';
function App() {
return (
<CurrentWidgetProvider>
<CurrentDatabaseProvider>
<SocketProvider>
<OpenedTabsProvider>
<OpenedConnectionsProvider>
<LeftPanelWidthProvider>
<ConnectionsPinger>
<PluginsProvider>
<ExtensionsProvider>
<CurrentArchiveProvider>
<CurrentThemeProvider>
<UploadsProvider>
<ModalLayerProvider>
<MenuLayerProvider>
<ThemeHelmet />
<Screen />
</MenuLayerProvider>
</ModalLayerProvider>
</UploadsProvider>
</CurrentThemeProvider>
</CurrentArchiveProvider>
</ExtensionsProvider>
</PluginsProvider>
</ConnectionsPinger>
</LeftPanelWidthProvider>
</OpenedConnectionsProvider>
</OpenedTabsProvider>
</SocketProvider>
</CurrentDatabaseProvider>
</CurrentWidgetProvider>
);
}
export default App;

View File

@@ -1,11 +0,0 @@
// @ts-nocheck
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,62 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { FontIcon } from './icons';
import useTheme from './theme/useTheme';
import getElectron from './utility/getElectron';
import useExtensions from './utility/useExtensions';
const TargetStyled = styled.div`
position: fixed;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${props => props.theme.main_background_blue[3]};
align-items: center;
justify-content: space-around;
z-index: 1000;
`;
const InfoBox = styled.div``;
const IconWrapper = styled.div`
display: flex;
justify-content: space-around;
font-size: 50px;
margin-bottom: 20px;
`;
const InfoWrapper = styled.div`
display: flex;
justify-content: space-around;
margin-top: 10px;
`;
const TitleWrapper = styled.div`
font-size: 30px;
display: flex;
justify-content: space-around;
`;
export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
const theme = useTheme();
const { fileFormats } = useExtensions();
const electron = getElectron();
const fileTypeNames = fileFormats.filter(x => x.readerFunc).map(x => x.name);
if (electron) fileTypeNames.push('SQL');
return (
!!isDragActive && (
<TargetStyled theme={theme}>
<InfoBox>
<IconWrapper>
<FontIcon icon="icon cloud-upload" />
</IconWrapper>
<TitleWrapper>Drop the files to upload to DbGate</TitleWrapper>
<InfoWrapper>Supported file types: {fileTypeNames.join(', ')}</InfoWrapper>
</InfoBox>
<input {...inputProps} />
</TargetStyled>
)
);
}

View File

@@ -1,147 +0,0 @@
// @ts-nocheck
import React from 'react';
import dimensions from './theme/dimensions';
import styled from 'styled-components';
import TabsPanel from './TabsPanel';
import TabContent from './TabContent';
import WidgetIconPanel from './widgets/WidgetIconPanel';
import { useCurrentWidget, useLeftPanelWidth, useSetLeftPanelWidth } from './utility/globalState';
import WidgetContainer from './widgets/WidgetContainer';
import ToolBar from './widgets/Toolbar';
import StatusBar from './widgets/StatusBar';
import { useSplitterDrag, HorizontalSplitHandle } from './widgets/Splitter';
import { ModalLayer } from './modals/showModal';
import DragAndDropFileTarget from './DragAndDropFileTarget';
import { useUploadsZone } from './utility/UploadsProvider';
import useTheme from './theme/useTheme';
import { MenuLayer } from './modals/showMenu';
import ErrorBoundary, { ErrorBoundaryTest } from './utility/ErrorBoundary';
const BodyDiv = styled.div`
position: fixed;
top: ${dimensions.tabsPanel.height + dimensions.toolBar.height}px;
left: ${props => props.contentLeft}px;
bottom: ${dimensions.statusBar.height}px;
right: 0;
background-color: ${props => props.theme.content_background};
`;
const ToolBarDiv = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: ${props => props.theme.toolbar_background};
height: ${dimensions.toolBar.height}px;
`;
const IconBar = styled.div`
position: fixed;
top: ${dimensions.toolBar.height}px;
left: 0;
bottom: ${dimensions.statusBar.height}px;
width: ${dimensions.widgetMenu.iconSize}px;
background-color: ${props => props.theme.widget_background};
`;
const LeftPanel = styled.div`
position: fixed;
top: ${dimensions.toolBar.height}px;
left: ${dimensions.widgetMenu.iconSize}px;
bottom: ${dimensions.statusBar.height}px;
background-color: ${props => props.theme.left_background};
display: flex;
`;
const TabsPanelContainer = styled.div`
display: flex;
position: fixed;
top: ${dimensions.toolBar.height}px;
left: ${props => props.contentLeft}px;
height: ${dimensions.tabsPanel.height}px;
right: 0;
background-color: ${props => props.theme.tabs_background2};
border-top: 1px solid ${props => props.theme.border};
overflow-x: auto;
::-webkit-scrollbar {
height: 7px;
}
}
`;
const StausBarContainer = styled.div`
position: fixed;
height: ${dimensions.statusBar.height}px;
left: 0;
right: 0;
bottom: 0;
background-color: ${props => props.theme.statusbar_background};
`;
const ScreenHorizontalSplitHandle = styled(HorizontalSplitHandle)`
position: absolute;
top: ${dimensions.toolBar.height}px;
bottom: ${dimensions.statusBar.height}px;
`;
// const StyledRoot = styled.div`
// // color: ${(props) => props.theme.fontColor};
// `;
export default function Screen() {
const theme = useTheme();
const currentWidget = useCurrentWidget();
const leftPanelWidth = useLeftPanelWidth();
const setLeftPanelWidth = useSetLeftPanelWidth();
const contentLeft = currentWidget
? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness
: dimensions.widgetMenu.iconSize;
const toolbarPortalRef = React.useRef();
const statusbarPortalRef = React.useRef();
const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff));
const { getRootProps, getInputProps, isDragActive } = useUploadsZone();
return (
<div {...getRootProps()}>
<ErrorBoundary>
<ToolBarDiv theme={theme}>
<ToolBar toolbarPortalRef={toolbarPortalRef} />
</ToolBarDiv>
<IconBar theme={theme}>
<WidgetIconPanel />
</IconBar>
{!!currentWidget && (
<LeftPanel theme={theme}>
<ErrorBoundary>
<WidgetContainer />
</ErrorBoundary>
</LeftPanel>
)}
{!!currentWidget && (
<ScreenHorizontalSplitHandle
onMouseDown={onSplitDown}
theme={theme}
style={{ left: leftPanelWidth + dimensions.widgetMenu.iconSize }}
/>
)}
<TabsPanelContainer contentLeft={contentLeft} theme={theme}>
<TabsPanel></TabsPanel>
</TabsPanelContainer>
<BodyDiv contentLeft={contentLeft} theme={theme}>
<TabContent toolbarPortalRef={toolbarPortalRef} statusbarPortalRef={statusbarPortalRef} />
</BodyDiv>
<StausBarContainer theme={theme}>
<StatusBar statusbarPortalRef={statusbarPortalRef} />
</StausBarContainer>
<ModalLayer />
<MenuLayer />
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
</ErrorBoundary>
</div>
);
}

View File

@@ -1,98 +0,0 @@
import React from 'react';
import _ from 'lodash';
import styled from 'styled-components';
import tabs from './tabs';
import { useOpenedTabs } from './utility/globalState';
import ErrorBoundary from './utility/ErrorBoundary';
const TabContainerStyled = styled.div`
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
visibility: ${props =>
// @ts-ignore
props.tabVisible ? 'visible' : 'hidden'};
`;
function TabContainer({ TabComponent, ...props }) {
const { tabVisible, tabid, toolbarPortalRef, statusbarPortalRef } = props;
return (
// @ts-ignore
<TabContainerStyled tabVisible={tabVisible}>
<ErrorBoundary>
<TabComponent
{...props}
tabid={tabid}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
statusbarPortalRef={statusbarPortalRef}
/>
</ErrorBoundary>
</TabContainerStyled>
);
}
const TabContainerMemo = React.memo(TabContainer);
function createTabComponent(selectedTab) {
const TabComponent = tabs[selectedTab.tabComponent];
if (TabComponent) {
return {
TabComponent,
props: selectedTab.props,
};
}
return null;
}
export default function TabContent({ toolbarPortalRef, statusbarPortalRef }) {
const files = useOpenedTabs();
const [mountedTabs, setMountedTabs] = React.useState({});
const selectedTab = files.find(x => x.selected && x.closedTime == null);
React.useEffect(() => {
// cleanup closed tabs
if (
_.difference(
_.keys(mountedTabs),
_.map(
files.filter(x => x.closedTime == null),
'tabid'
)
).length > 0
) {
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find(x => x.tabid == k && x.closedTime == null)));
}
if (selectedTab) {
const { tabid } = selectedTab;
if (tabid && !mountedTabs[tabid])
setMountedTabs({
...mountedTabs,
[tabid]: createTabComponent(selectedTab),
});
}
}, [mountedTabs, files]);
return _.keys(mountedTabs).map(tabid => {
const { TabComponent, props } = mountedTabs[tabid];
const tabVisible = tabid == (selectedTab && selectedTab.tabid);
return (
<TabContainerMemo
key={tabid}
{...props}
tabid={tabid}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
statusbarPortalRef={statusbarPortalRef}
TabComponent={TabComponent}
/>
);
});
}

View File

@@ -1,300 +0,0 @@
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { DropDownMenuItem, DropDownMenuDivider } from './modals/DropDownMenu';
import { useOpenedTabs, useSetOpenedTabs, useCurrentDatabase, useSetCurrentDatabase } from './utility/globalState';
import { getConnectionInfo } from './utility/metadataLoaders';
import { FontIcon } from './icons';
import useTheme from './theme/useTheme';
import usePropsCompare from './utility/usePropsCompare';
import { useShowMenu } from './modals/showMenu';
import { setSelectedTabFunc } from './utility/common';
import getElectron from './utility/getElectron';
// const files = [
// { name: 'app.js' },
// { name: 'BranchCategory', type: 'table', selected: true },
// { name: 'ApplicationList' },
// ];
const DbGroupHandler = styled.div`
display: flex;
flex: 1;
align-content: stretch;
`;
const DbWrapperHandler = styled.div`
display: flex;
flex-direction: column;
align-items: stretch;
`;
const DbNameWrapper = styled.div`
text-align: center;
font-size: 8pt;
border-bottom: 1px solid ${props => props.theme.border};
border-right: 1px solid ${props => props.theme.border};
cursor: pointer;
user-select: none;
padding: 1px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
// height: 15px;
&:hover {
background-color: ${props => props.theme.tabs_background3};
}
background-color: ${props =>
// @ts-ignore
props.selected ? props.theme.tabs_background1 : 'inherit'};
`;
// const DbNameWrapperInner = styled.div`
// position: absolute;
// white-space: nowrap;
// `;
const FileTabItem = styled.div`
border-right: 1px solid ${props => props.theme.border};
padding-left: 15px;
padding-right: 15px;
flex-shrink: 1;
flex-grow: 1;
min-width: 10px;
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
&:hover {
color: ${props => props.theme.tabs_font_hover};
}
background-color: ${props =>
// @ts-ignore
props.selected ? props.theme.tabs_background1 : 'inherit'};
`;
const FileNameWrapper = styled.span`
margin-left: 5px;
`;
const CloseButton = styled(FontIcon)`
margin-left: 5px;
color: gray;
&:hover {
color: ${props => props.theme.tabs_font2};
}
`;
function TabContextMenu({ close, closeAll, closeOthers, closeWithSameDb, closeWithOtherDb, props }) {
const { database } = props || {};
const { conid } = props || {};
return (
<>
<DropDownMenuItem onClick={close}>Close</DropDownMenuItem>
<DropDownMenuItem onClick={closeAll}>Close all</DropDownMenuItem>
<DropDownMenuItem onClick={closeOthers}>Close others</DropDownMenuItem>
{conid && database && (
<DropDownMenuItem onClick={closeWithSameDb}>Close with same DB - {database}</DropDownMenuItem>
)}
{conid && database && (
<DropDownMenuItem onClick={closeWithOtherDb}>Close with other DB than {database}</DropDownMenuItem>
)}
</>
);
}
function getTabDbName(tab) {
if (tab.props && tab.props.conid && tab.props.database) return tab.props.database;
if (tab.props && tab.props.archiveFolder) return tab.props.archiveFolder;
return '(no DB)';
}
function getTabDbKey(tab) {
if (tab.props && tab.props.conid && tab.props.database) return `database://${tab.props.database}-${tab.props.conid}`;
if (tab.props && tab.props.archiveFolder) return `archive://${tab.props.archiveFolder}`;
return '_no';
}
function getDbIcon(key) {
if (key.startsWith('database://')) return 'icon database';
if (key.startsWith('archive://')) return 'icon archive';
return 'icon file';
}
function buildTooltip(tab) {
let res = tab.tooltip;
if (tab.props && tab.props.savedFilePath) {
if (res) res += '\n';
res += tab.props.savedFilePath;
}
return res;
}
export default function TabsPanel() {
// const formatDbKey = (conid, database) => `${database}-${conid}`;
const theme = useTheme();
const showMenu = useShowMenu();
const tabs = useOpenedTabs();
const setOpenedTabs = useSetOpenedTabs();
const currentDb = useCurrentDatabase();
const setCurrentDb = useSetCurrentDatabase();
const { name, connection } = currentDb || {};
const currentDbKey = name && connection ? `database://${name}-${connection._id}` : '_no';
const handleTabClick = (e, tabid) => {
if (e.target.closest('.tabCloseButton')) {
return;
}
setOpenedTabs(files => setSelectedTabFunc(files, tabid));
};
const closeTabFunc = closeCondition => tabid => {
setOpenedTabs(files => {
const active = files.find(x => x.tabid == tabid);
if (!active) return files;
const newFiles = files.map(x => ({
...x,
closedTime: x.closedTime || (closeCondition(x, active) ? new Date().getTime() : undefined),
}));
if (newFiles.find(x => x.selected && x.closedTime == null)) {
return newFiles;
}
const selectedIndex = _.findLastIndex(newFiles, x => x.closedTime == null);
return newFiles.map((x, index) => ({
...x,
selected: index == selectedIndex,
}));
});
};
const closeTab = closeTabFunc((x, active) => x.tabid == active.tabid);
const closeAll = () => {
const closedTime = new Date().getTime();
setOpenedTabs(tabs =>
tabs.map(tab => ({
...tab,
closedTime: tab.closedTime || closedTime,
selected: false,
}))
);
};
const closeWithSameDb = closeTabFunc(
(x, active) =>
_.get(x, 'props.conid') == _.get(active, 'props.conid') &&
_.get(x, 'props.database') == _.get(active, 'props.database')
);
const closeWithOtherDb = closeTabFunc(
(x, active) =>
_.get(x, 'props.conid') != _.get(active, 'props.conid') ||
_.get(x, 'props.database') != _.get(active, 'props.database')
);
const closeOthers = closeTabFunc((x, active) => x.tabid != active.tabid);
const handleMouseUp = (e, tabid) => {
if (e.button == 1) {
e.preventDefault();
closeTab(tabid);
}
};
const handleContextMenu = (event, tabid, props) => {
event.preventDefault();
showMenu(
event.pageX,
event.pageY,
<TabContextMenu
close={() => closeTab(tabid)}
closeAll={closeAll}
closeOthers={() => closeOthers(tabid)}
closeWithSameDb={() => closeWithSameDb(tabid)}
closeWithOtherDb={() => closeWithOtherDb(tabid)}
props={props}
/>
);
};
React.useEffect(() => {
const electron = getElectron();
if (electron) {
const { ipcRenderer } = electron;
const activeTab = tabs.find(x => x.selected);
window['dbgate_activeTabId'] = activeTab ? activeTab.tabid : null;
ipcRenderer.send('update-menu');
}
}, [tabs]);
// console.log(
// 't',
// tabs.map(x => x.tooltip)
// );
const tabsWithDb = tabs
.filter(x => !x.closedTime)
.map(tab => ({
...tab,
tabDbName: getTabDbName(tab),
tabDbKey: getTabDbKey(tab),
}));
const tabsByDb = _.groupBy(tabsWithDb, 'tabDbKey');
const dbKeys = _.keys(tabsByDb).sort();
const handleSetDb = async props => {
const { conid, database } = props || {};
if (conid) {
const connection = await getConnectionInfo({ conid, database });
if (connection) {
setCurrentDb({ connection, name: database });
return;
}
}
setCurrentDb(null);
};
return (
<>
{dbKeys.map(dbKey => (
<DbWrapperHandler key={dbKey}>
<DbNameWrapper
// @ts-ignore
selected={tabsByDb[dbKey][0].tabDbKey == currentDbKey}
onClick={() => handleSetDb(tabsByDb[dbKey][0].props)}
theme={theme}
>
<FontIcon icon={getDbIcon(dbKey)} /> {tabsByDb[dbKey][0].tabDbName}
</DbNameWrapper>
<DbGroupHandler>
{_.sortBy(tabsByDb[dbKey], ['title', 'tabid']).map(tab => (
<FileTabItem
{...tab}
title={buildTooltip(tab)}
key={tab.tabid}
theme={theme}
onClick={e => handleTabClick(e, tab.tabid)}
onMouseUp={e => handleMouseUp(e, tab.tabid)}
onContextMenu={e => handleContextMenu(e, tab.tabid, tab.props)}
>
{<FontIcon icon={tab.busy ? 'icon loading' : tab.icon} />}
<FileNameWrapper>{tab.title}</FileNameWrapper>
<CloseButton
icon="icon close"
className="tabCloseButton"
theme={theme}
onClick={e => {
e.preventDefault();
closeTab(tab.tabid);
}}
/>
</FileTabItem>
))}
</DbGroupHandler>
</DbWrapperHandler>
))}
</>
);
}

View File

@@ -1,97 +0,0 @@
// @ts-nocheck
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import { useShowMenu } from '../modals/showMenu';
import useTheme from '../theme/useTheme';
const AppObjectDiv = styled.div`
padding: 5px;
${props =>
!props.disableHover &&
`
&:hover {
background-color: ${props.theme.left_background_blue[1]};
}
`}
cursor: pointer;
white-space: nowrap;
font-weight: ${props => (props.isBold ? 'bold' : 'normal')};
`;
const IconWrap = styled.span`
margin-right: 5px;
`;
const StatusIconWrap = styled.span`
margin-left: 5px;
`;
const ExtInfoWrap = styled.span`
font-weight: normal;
margin-left: 5px;
color: ${props => props.theme.left_font3};
`;
export function AppObjectCore({
title,
icon,
data,
onClick = undefined,
onClick2 = undefined,
onClick3 = undefined,
isBold = undefined,
isBusy = undefined,
prefix = undefined,
statusIcon = undefined,
extInfo = undefined,
statusTitle = undefined,
disableHover = false,
children = null,
Menu = undefined,
...other
}) {
const theme = useTheme();
const showMenu = useShowMenu();
const handleContextMenu = event => {
if (!Menu) return;
event.preventDefault();
showMenu(event.pageX, event.pageY, <Menu data={data} />);
};
return (
<>
<AppObjectDiv
onContextMenu={handleContextMenu}
onClick={() => {
if (onClick) onClick(data);
if (onClick2) onClick2(data);
if (onClick3) onClick3(data);
}}
theme={theme}
isBold={isBold}
draggable
onDragStart={e => {
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
}}
disableHover={disableHover}
{...other}
>
{prefix}
<IconWrap>{isBusy ? <FontIcon icon="icon loading" /> : <FontIcon icon={icon} />}</IconWrap>
{title}
{statusIcon && (
<StatusIconWrap>
<FontIcon icon={statusIcon} title={statusTitle} />
</StatusIconWrap>
)}
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
</AppObjectDiv>
{children}
</>
);
}

View File

@@ -1,164 +0,0 @@
import React from 'react';
import _ from 'lodash';
import styled from 'styled-components';
import { ExpandIcon } from '../icons';
import useTheme from '../theme/useTheme';
const SubItemsDiv = styled.div`
margin-left: 28px;
`;
const ExpandIconHolder2 = styled.span`
margin-right: 3px;
// position: relative;
// top: -3px;
`;
const ExpandIconHolder = styled.span`
margin-right: 5px;
`;
const GroupDiv = styled.div`
user-select: none;
padding: 5px;
&:hover {
background-color: ${props => props.theme.left_background_blue[1]};
}
cursor: pointer;
white-space: nowrap;
font-weight: bold;
`;
function AppObjectListItem({
AppObjectComponent,
data,
filter,
onObjectClick,
isExpandable,
SubItems,
getCommonProps,
expandOnClick,
ExpandIconComponent,
}) {
const [isExpanded, setIsExpanded] = React.useState(false);
const expandable = data && isExpandable && isExpandable(data);
React.useEffect(() => {
if (!expandable) {
setIsExpanded(false);
}
}, [expandable]);
let commonProps = {
prefix: SubItems ? (
<ExpandIconHolder2>
{expandable ? (
<ExpandIconComponent
isExpanded={isExpanded}
onClick={e => {
setIsExpanded(v => !v);
e.stopPropagation();
}}
/>
) : (
<ExpandIconComponent isBlank />
)}
</ExpandIconHolder2>
) : null,
};
if (SubItems && expandOnClick) {
commonProps.onClick2 = () => setIsExpanded(v => !v);
}
if (onObjectClick) {
commonProps.onClick3 = onObjectClick;
}
if (getCommonProps) {
commonProps = { ...commonProps, ...getCommonProps(data) };
}
let res = <AppObjectComponent data={data} commonProps={commonProps} />;
if (SubItems && isExpanded) {
res = (
<>
{res}
<SubItemsDiv>
<SubItems data={data} filter={filter} />
</SubItemsDiv>
</>
);
}
return res;
}
function AppObjectGroup({ group, items }) {
const [isExpanded, setIsExpanded] = React.useState(true);
const theme = useTheme();
const filtered = items.filter(x => x.component);
let countText = filtered.length.toString();
if (filtered.length < items.length) countText += `/${items.length}`;
return (
<>
<GroupDiv onClick={() => setIsExpanded(!isExpanded)} theme={theme}>
<ExpandIconHolder>
<ExpandIcon isExpanded={isExpanded} />
</ExpandIconHolder>
{group} {items && `(${countText})`}
</GroupDiv>
{isExpanded && filtered.map(x => x.component)}
</>
);
}
export function AppObjectList({
list,
AppObjectComponent,
SubItems = undefined,
onObjectClick = undefined,
filter = undefined,
groupFunc = undefined,
groupOrdered = undefined,
isExpandable = undefined,
getCommonProps = undefined,
expandOnClick = false,
ExpandIconComponent = ExpandIcon,
}) {
const createComponent = data => (
<AppObjectListItem
key={AppObjectComponent.extractKey(data)}
AppObjectComponent={AppObjectComponent}
data={data}
filter={filter}
onObjectClick={onObjectClick}
SubItems={SubItems}
isExpandable={isExpandable}
getCommonProps={getCommonProps}
expandOnClick={expandOnClick}
ExpandIconComponent={ExpandIconComponent}
/>
);
if (groupFunc) {
const listGrouped = _.compact(
(list || []).map(data => {
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
const component = matcher && !matcher(filter) ? null : createComponent(data);
const group = groupFunc(data);
return { group, data, component };
})
);
const groups = _.groupBy(listGrouped, 'group');
return (groupOrdered || _.keys(groups)).map(group => (
<AppObjectGroup key={group} group={group} items={groups[group]} />
));
}
return (list || []).map(data => {
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
if (matcher && !matcher(filter)) return null;
return createComponent(data);
});
}

View File

@@ -1,75 +0,0 @@
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { filterName } from 'dbgate-datalib';
import axios from '../utility/axios';
import { AppObjectCore } from './AppObjectCore';
import useOpenNewTab from '../utility/useOpenNewTab';
function openArchive(openNewTab, fileName, folderName) {
openNewTab({
title: fileName,
icon: 'img archive',
tooltip: `${folderName}\n${fileName}`,
tabComponent: 'ArchiveFileTab',
props: {
archiveFile: fileName,
archiveFolder: folderName,
},
});
}
function Menu({ data }) {
const openNewTab = useOpenNewTab();
const handleDelete = () => {
axios.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
};
const handleOpenRead = () => {
openArchive(openNewTab, data.fileName, data.folderName);
};
const handleOpenWrite = async () => {
// const resp = await axios.post('archive/load-free-table', { file: data.fileName, folder: data.folderName });
openNewTab({
title: data.fileName,
icon: 'img archive',
tabComponent: 'FreeTableTab',
props: {
initialArgs: {
functionName: 'archiveReader',
props: {
fileName: data.fileName,
folderName: data.folderName,
},
},
archiveFile: data.fileName,
archiveFolder: data.folderName,
},
});
};
return (
<>
<DropDownMenuItem onClick={handleOpenRead}>Open (readonly)</DropDownMenuItem>
<DropDownMenuItem onClick={handleOpenWrite}>Open in free table editor</DropDownMenuItem>
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
</>
);
}
function ArchiveFileAppObject({ data, commonProps }) {
const { fileName, folderName } = data;
const openNewTab = useOpenNewTab();
const onClick = () => {
openArchive(openNewTab, fileName, folderName);
};
return (
<AppObjectCore {...commonProps} data={data} title={fileName} icon="img archive" onClick={onClick} Menu={Menu} />
);
}
ArchiveFileAppObject.extractKey = data => data.fileName;
ArchiveFileAppObject.createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
export default ArchiveFileAppObject;

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import axios from '../utility/axios';
import { filterName } from 'dbgate-datalib';
import { AppObjectCore } from './AppObjectCore';
import { useCurrentArchive } from '../utility/globalState';
function Menu({ data }) {
const handleDelete = () => {
axios.post('archive/delete-folder', { folder: data.name });
};
return <>{data.name != 'default' && <DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>}</>;
}
function ArchiveFolderAppObject({ data, commonProps }) {
const { name } = data;
const currentArchive = useCurrentArchive();
return (
<AppObjectCore
{...commonProps}
data={data}
title={name}
icon="img archive-folder"
isBold={name == currentArchive}
Menu={Menu}
/>
);
}
ArchiveFolderAppObject.extractKey = data => data.name;
ArchiveFolderAppObject.createMatcher = data => filter => filterName(filter, data.name);
export default ArchiveFolderAppObject;

View File

@@ -1,73 +0,0 @@
import React from 'react';
import _ from 'lodash';
import moment from 'moment';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { useSetOpenedTabs } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore';
import { setSelectedTabFunc } from '../utility/common';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
const InfoDiv = styled.div`
margin-left: 30px;
color: ${props => props.theme.left_font3};
`;
function Menu({ data }) {
const setOpenedTabs = useSetOpenedTabs();
const handleDelete = () => {
setOpenedTabs(tabs => tabs.filter(x => x.tabid != data.tabid));
};
const handleDeleteOlder = () => {
setOpenedTabs(tabs => tabs.filter(x => !x.closedTime || x.closedTime >= data.closedTime));
};
return (
<>
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
<DropDownMenuItem onClick={handleDeleteOlder}>Delete older</DropDownMenuItem>
</>
);
}
function ClosedTabAppObject({ data, commonProps }) {
const { tabid, props, selected, icon, title, closedTime, busy } = data;
const setOpenedTabs = useSetOpenedTabs();
const theme = useTheme();
const onClick = () => {
setOpenedTabs(files =>
setSelectedTabFunc(
files.map(x => ({
...x,
closedTime: x.tabid == tabid ? undefined : x.closedTime,
})),
tabid
)
);
};
return (
<AppObjectCore
{...commonProps}
data={data}
title={`${title} ${moment(closedTime).fromNow()}`}
icon={icon}
isBold={!!selected}
onClick={onClick}
isBusy={busy}
Menu={Menu}
>
{data.props && data.props.database && (
<InfoDiv theme={theme}>
<FontIcon icon="icon database" /> {data.props.database}
</InfoDiv>
)}
{data.contentPreview && <InfoDiv theme={theme}>{data.contentPreview}</InfoDiv>}
</AppObjectCore>
);
}
ClosedTabAppObject.extractKey = data => data.tabid;
export default ClosedTabAppObject;

View File

@@ -1,119 +0,0 @@
import _ from 'lodash';
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import ConnectionModal from '../modals/ConnectionModal';
import axios from '../utility/axios';
import { filterName } from 'dbgate-datalib';
import ConfirmModal from '../modals/ConfirmModal';
import CreateDatabaseModal from '../modals/CreateDatabaseModal';
import { useCurrentDatabase, useOpenedConnections, useSetOpenedConnections } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore';
import useShowModal from '../modals/showModal';
import { useConfig } from '../utility/metadataLoaders';
import useExtensions from '../utility/useExtensions';
function Menu({ data }) {
const openedConnections = useOpenedConnections();
const setOpenedConnections = useSetOpenedConnections();
const showModal = useShowModal();
const config = useConfig();
const handleEdit = () => {
showModal(modalState => <ConnectionModal modalState={modalState} connection={data} />);
};
const handleDelete = () => {
showModal(modalState => (
<ConfirmModal
modalState={modalState}
message={`Really delete connection ${data.displayName || data.server}?`}
onConfirm={() => axios.post('connections/delete', data)}
/>
));
};
const handleCreateDatabase = () => {
showModal(modalState => <CreateDatabaseModal modalState={modalState} conid={data._id} />);
};
const handleRefresh = () => {
axios.post('server-connections/refresh', { conid: data._id });
};
const handleDisconnect = () => {
setOpenedConnections(list => list.filter(x => x != data._id));
};
const handleConnect = () => {
setOpenedConnections(list => _.uniq([...list, data._id]));
};
return (
<>
{config.runAsPortal == false && (
<>
<DropDownMenuItem onClick={handleEdit}>Edit</DropDownMenuItem>
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
</>
)}
{!openedConnections.includes(data._id) && <DropDownMenuItem onClick={handleConnect}>Connect</DropDownMenuItem>}
{openedConnections.includes(data._id) && data.status && (
<DropDownMenuItem onClick={handleRefresh}>Refresh</DropDownMenuItem>
)}
{openedConnections.includes(data._id) && (
<DropDownMenuItem onClick={handleDisconnect}>Disconnect</DropDownMenuItem>
)}
{openedConnections.includes(data._id) && (
<DropDownMenuItem onClick={handleCreateDatabase}>Create database</DropDownMenuItem>
)}
</>
);
}
function ConnectionAppObject({ data, commonProps }) {
const { _id, server, displayName, engine, status } = data;
const openedConnections = useOpenedConnections();
const setOpenedConnections = useSetOpenedConnections();
const currentDatabase = useCurrentDatabase();
const extensions = useExtensions();
const isBold = _.get(currentDatabase, 'connection._id') == _id;
const onClick = () => setOpenedConnections(c => _.uniq([...c, _id]));
let statusIcon = null;
let statusTitle = null;
let extInfo = null;
if (extensions.drivers.find(x => x.engine == engine)) {
const match = (engine || '').match(/^([^@]*)@/);
extInfo = match ? match[1] : engine;
} else {
extInfo = engine;
statusIcon = 'img warn';
statusTitle = `Engine driver ${engine} not found, review installed plugins and change engine in edit connection dialog`;
}
if (openedConnections.includes(_id)) {
if (!status) statusIcon = 'icon loading';
else if (status.name == 'pending') statusIcon = 'icon loading';
else if (status.name == 'ok') statusIcon = 'img ok';
else statusIcon = 'img error';
if (status && status.name == 'error') {
statusTitle = status.message;
}
}
return (
<AppObjectCore
{...commonProps}
title={displayName || server}
icon="img server"
data={data}
statusIcon={statusIcon}
statusTitle={statusTitle}
extInfo={extInfo}
isBold={isBold}
onClick={onClick}
Menu={Menu}
/>
);
}
ConnectionAppObject.extractKey = data => data._id;
ConnectionAppObject.createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server);
export default ConnectionAppObject;

View File

@@ -1,90 +0,0 @@
import React from 'react';
import _ from 'lodash';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import ImportExportModal from '../modals/ImportExportModal';
import { getDefaultFileFormat } from '../utility/fileformats';
import { useCurrentDatabase } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore';
import useShowModal from '../modals/showModal';
import useExtensions from '../utility/useExtensions';
import useOpenNewTab from '../utility/useOpenNewTab';
function Menu({ data }) {
const { connection, name } = data;
const openNewTab = useOpenNewTab();
const extensions = useExtensions();
const showModal = useShowModal();
const tooltip = `${connection.displayName || connection.server}\n${name}`;
const handleNewQuery = () => {
openNewTab({
title: 'Query #',
icon: 'img sql-file',
tooltip,
tabComponent: 'QueryTab',
props: {
conid: connection._id,
database: name,
},
});
};
const handleImport = () => {
showModal(modalState => (
<ImportExportModal
modalState={modalState}
initialValues={{
sourceStorageType: getDefaultFileFormat(extensions).storageType,
targetStorageType: 'database',
targetConnectionId: connection._id,
targetDatabaseName: name,
}}
/>
));
};
const handleExport = () => {
showModal(modalState => (
<ImportExportModal
modalState={modalState}
initialValues={{
targetStorageType: getDefaultFileFormat(extensions).storageType,
sourceStorageType: 'database',
sourceConnectionId: connection._id,
sourceDatabaseName: name,
}}
/>
));
};
return (
<>
<DropDownMenuItem onClick={handleNewQuery}>New query</DropDownMenuItem>
<DropDownMenuItem onClick={handleImport}>Import</DropDownMenuItem>
<DropDownMenuItem onClick={handleExport}>Export</DropDownMenuItem>
</>
);
}
function DatabaseAppObject({ data, commonProps }) {
const { name, connection } = data;
const currentDatabase = useCurrentDatabase();
return (
<AppObjectCore
{...commonProps}
data={data}
title={name}
icon="img database"
isBold={
_.get(currentDatabase, 'connection._id') == _.get(connection, '_id') && _.get(currentDatabase, 'name') == name
}
Menu={Menu}
/>
);
}
DatabaseAppObject.extractKey = props => props.name;
export default DatabaseAppObject;

View File

@@ -1,325 +0,0 @@
import _ from 'lodash';
import React from 'react';
import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu';
import { getConnectionInfo } from '../utility/metadataLoaders';
import fullDisplayName from '../utility/fullDisplayName';
import { filterName } from 'dbgate-datalib';
import ImportExportModal from '../modals/ImportExportModal';
import { useSetOpenedTabs } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore';
import useShowModal from '../modals/showModal';
import { findEngineDriver } from 'dbgate-tools';
import useExtensions from '../utility/useExtensions';
import useOpenNewTab from '../utility/useOpenNewTab';
import uuidv1 from 'uuid/v1';
import { AppObjectList } from './AppObjectList';
const icons = {
tables: 'img table',
views: 'img view',
procedures: 'img procedure',
functions: 'img function',
};
const menus = {
tables: [
{
label: 'Open data',
tab: 'TableDataTab',
forceNewTab: true,
},
{
label: 'Open form',
tab: 'TableDataTab',
forceNewTab: true,
initialData: {
grid: {
isFormView: true,
},
},
},
{
label: 'Open structure',
tab: 'TableStructureTab',
},
{
label: 'Query designer',
isQueryDesigner: true,
},
{
isDivider: true,
},
{
label: 'Export',
isExport: true,
},
{
label: 'Open in free table editor',
isOpenFreeTable: true,
},
{
label: 'Open active chart',
isActiveChart: true,
},
{
isDivider: true,
},
{
label: 'SQL: CREATE TABLE',
sqlTemplate: 'CREATE TABLE',
},
],
views: [
{
label: 'Open data',
tab: 'ViewDataTab',
forceNewTab: true,
},
{
label: 'Open structure',
tab: 'TableStructureTab',
},
{
label: 'Query designer',
isQueryDesigner: true,
},
{
isDivider: true,
},
{
label: 'Export',
isExport: true,
},
{
label: 'Open in free table editor',
isOpenFreeTable: true,
},
{
label: 'Open active chart',
isActiveChart: true,
},
{
isDivider: true,
},
{
label: 'SQL: CREATE VIEW',
sqlTemplate: 'CREATE OBJECT',
},
{
label: 'SQL: CREATE TABLE',
sqlTemplate: 'CREATE TABLE',
},
],
procedures: [
{
label: 'SQL: CREATE PROCEDURE',
sqlTemplate: 'CREATE OBJECT',
},
{
label: 'SQL: EXECUTE',
sqlTemplate: 'EXECUTE PROCEDURE',
},
],
functions: [
{
label: 'SQL: CREATE FUNCTION',
sqlTemplate: 'CREATE OBJECT',
},
],
};
const defaultTabs = {
tables: 'TableDataTab',
views: 'ViewDataTab',
};
export async function openDatabaseObjectDetail(
openNewTab,
tabComponent,
sqlTemplate,
{ schemaName, pureName, conid, database, objectTypeField },
forceNewTab,
initialData
) {
const connection = await getConnectionInfo({ conid });
const tooltip = `${connection.displayName || connection.server}\n${database}\n${fullDisplayName({
schemaName,
pureName,
})}`;
openNewTab(
{
title: sqlTemplate ? 'Query #' : pureName,
tooltip,
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
props: {
schemaName,
pureName,
conid,
database,
objectTypeField,
initialArgs: sqlTemplate ? { sqlTemplate } : null,
},
},
initialData,
{ forceNewTab }
);
}
function Menu({ data }) {
const showModal = useShowModal();
const openNewTab = useOpenNewTab();
const extensions = useExtensions();
const getDriver = async () => {
const conn = await getConnectionInfo(data);
if (!conn) return;
const driver = findEngineDriver(conn, extensions);
return driver;
};
return (
<>
{menus[data.objectTypeField].map(menu =>
menu.isDivider ? (
<DropDownMenuDivider />
) : (
<DropDownMenuItem
key={menu.label}
onClick={async () => {
if (menu.isExport) {
showModal(modalState => (
<ImportExportModal
modalState={modalState}
initialValues={{
sourceStorageType: 'database',
sourceConnectionId: data.conid,
sourceDatabaseName: data.database,
sourceSchemaName: data.schemaName,
sourceList: [data.pureName],
}}
/>
));
} else if (menu.isOpenFreeTable) {
const coninfo = await getConnectionInfo(data);
openNewTab({
title: data.pureName,
icon: 'img free-table',
tabComponent: 'FreeTableTab',
props: {
initialArgs: {
functionName: 'tableReader',
props: {
connection: {
...coninfo,
database: data.database,
},
schemaName: data.schemaName,
pureName: data.pureName,
},
},
},
});
} else if (menu.isActiveChart) {
const driver = await getDriver();
const dmp = driver.createDumper();
dmp.put('^select * from %f', data);
openNewTab(
{
title: data.pureName,
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
config: { chartType: 'bar' },
sql: dmp.s,
},
}
);
} else if (menu.isQueryDesigner) {
openNewTab(
{
title: 'Query #',
icon: 'img query-design',
tabComponent: 'QueryDesignTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
tables: [
{
...data,
designerId: uuidv1(),
left: 50,
top: 50,
},
],
},
}
);
} else {
openDatabaseObjectDetail(
openNewTab,
menu.tab,
menu.sqlTemplate,
data,
menu.forceNewTab,
menu.initialData
);
}
}}
>
{menu.label}
</DropDownMenuItem>
)
)}
</>
);
}
function DatabaseObjectAppObject({ data, commonProps }) {
const { conid, database, pureName, schemaName, objectTypeField } = data;
const openNewTab = useOpenNewTab();
const onClick = ({ schemaName, pureName }) => {
openDatabaseObjectDetail(
openNewTab,
defaultTabs[objectTypeField],
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
{
schemaName,
pureName,
conid,
database,
objectTypeField,
},
false
);
};
return (
<AppObjectCore
{...commonProps}
data={data}
title={schemaName ? `${schemaName}.${pureName}` : pureName}
icon={icons[objectTypeField]}
onClick={onClick}
Menu={Menu}
/>
);
}
DatabaseObjectAppObject.extractKey = ({ schemaName, pureName }) =>
schemaName ? `${schemaName}.${pureName}` : pureName;
DatabaseObjectAppObject.createMatcher = ({ pureName }) => filter => filterName(filter, pureName);
export default DatabaseObjectAppObject;

View File

@@ -1,104 +0,0 @@
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import FavoriteModal from '../modals/FavoriteModal';
import useShowModal from '../modals/showModal';
import axios from '../utility/axios';
import { copyTextToClipboard } from '../utility/clipboard';
import getElectron from '../utility/getElectron';
import useOpenNewTab from '../utility/useOpenNewTab';
import { SavedFileAppObjectBase } from './SavedFileAppObject';
export function useOpenFavorite() {
const openNewTab = useOpenNewTab();
const openFavorite = React.useCallback(
async favorite => {
const { icon, tabComponent, title, props, tabdata } = favorite;
let tabdataNew = tabdata;
if (props.savedFile) {
const resp = await axios.post('files/load', {
folder: props.savedFolder,
file: props.savedFile,
format: props.savedFormat,
});
tabdataNew = {
...tabdata,
editor: resp.data,
};
}
openNewTab(
{
title,
icon: icon || 'img favorite',
props,
tabComponent,
},
tabdataNew
);
},
[openNewTab]
);
return openFavorite;
}
export function FavoriteFileAppObject({ data, commonProps }) {
const { icon, tabComponent, title, props, tabdata, urlPath } = data;
const openNewTab = useOpenNewTab();
const showModal = useShowModal();
const openFavorite = useOpenFavorite();
const electron = getElectron();
const editFavorite = () => {
showModal(modalState => <FavoriteModal modalState={modalState} editingData={data} />);
};
const editFavoriteJson = async () => {
const resp = await axios.post('files/load', {
folder: 'favorites',
file: data.file,
format: 'text',
});
openNewTab(
{
icon: 'icon favorite',
title,
tabComponent: 'FavoriteEditorTab',
props: {
savedFile: data.file,
savedFormat: 'text',
savedFolder: 'favorites',
},
},
{ editor: JSON.stringify(JSON.parse(resp.data), null, 2) }
);
};
const copyLink = () => {
copyTextToClipboard(`${document.location.origin}#favorite=${urlPath}`);
};
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="json"
icon={icon || 'img favorite'}
title={title}
disableRename
onLoad={async data => {
openFavorite(data);
}}
menuExt={
<>
<DropDownMenuItem onClick={editFavorite}>Edit</DropDownMenuItem>
<DropDownMenuItem onClick={editFavoriteJson}>Edit JSON definition</DropDownMenuItem>
{!electron && urlPath && <DropDownMenuItem onClick={copyLink}>Copy link</DropDownMenuItem>}
</>
}
/>
);
}
FavoriteFileAppObject.extractKey = data => data.file;

View File

@@ -1,15 +0,0 @@
import _ from 'lodash';
import React from 'react';
import { filterName } from 'dbgate-datalib';
import { AppObjectCore } from './AppObjectCore';
function MacroAppObject({ data, commonProps }) {
const { name, type, title, group } = data;
return <AppObjectCore {...commonProps} data={data} title={title} icon={'img macro'} />;
}
MacroAppObject.extractKey = data => data.name;
MacroAppObject.createMatcher = ({ name, title }) => filter => filterName(filter, name, title);
export default MacroAppObject;

View File

@@ -1,314 +0,0 @@
import React from 'react';
import axios from '../utility/axios';
import _ from 'lodash';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { AppObjectCore } from './AppObjectCore';
import useNewQuery from '../query/useNewQuery';
import { useCurrentDatabase } from '../utility/globalState';
import ScriptWriter from '../impexp/ScriptWriter';
import { extractPackageName } from 'dbgate-tools';
import useShowModal from '../modals/showModal';
import InputTextModal from '../modals/InputTextModal';
import useHasPermission from '../utility/useHasPermission';
import useOpenNewTab from '../utility/useOpenNewTab';
import ConfirmModal from '../modals/ConfirmModal';
function Menu({ data, menuExt = null, title = undefined, disableRename = false }) {
const hasPermission = useHasPermission();
const showModal = useShowModal();
const handleDelete = () => {
showModal(modalState => (
<ConfirmModal
modalState={modalState}
message={`Really delete file ${title || data.file}?`}
onConfirm={() => {
axios.post('files/delete', data);
}}
/>
));
};
const handleRename = () => {
showModal(modalState => (
<InputTextModal
modalState={modalState}
value={data.file}
label="New file name"
header="Rename file"
onConfirm={newFile => {
axios.post('files/rename', { ...data, newFile });
}}
/>
));
};
return (
<>
{hasPermission(`files/${data.folder}/write`) && (
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
)}
{hasPermission(`files/${data.folder}/write`) && !disableRename && (
<DropDownMenuItem onClick={handleRename}>Rename</DropDownMenuItem>
)}
{menuExt}
</>
);
}
export function SavedFileAppObjectBase({
data,
commonProps,
format,
icon,
onLoad,
title = undefined,
menuExt = null,
disableRename = false,
}) {
const { file, folder } = data;
const onClick = async () => {
const resp = await axios.post('files/load', { folder, file, format });
onLoad(resp.data);
};
return (
<AppObjectCore
{...commonProps}
data={data}
title={title || file}
icon={icon}
onClick={onClick}
Menu={props => <Menu {...props} menuExt={menuExt} title={title} disableRename={disableRename} />}
/>
);
}
export function SavedSqlFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const newQuery = useNewQuery();
const currentDatabase = useCurrentDatabase();
const openNewTab = useOpenNewTab();
const connection = _.get(currentDatabase, 'connection');
const database = _.get(currentDatabase, 'name');
const handleGenerateExecute = () => {
const script = new ScriptWriter();
const conn = {
..._.omit(connection, ['displayName', '_id']),
database,
};
script.put(`const sql = await dbgateApi.loadFile('${folder}/${file}');`);
script.put(`await dbgateApi.executeQuery({ sql, connection: ${JSON.stringify(conn)} });`);
// @ts-ignore
script.requirePackage(extractPackageName(conn.engine));
openNewTab(
{
title: 'Shell #',
icon: 'img shell',
tabComponent: 'ShellTab',
},
{ editor: script.getScript() }
);
};
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="text"
icon="img sql-file"
menuExt={
connection && database ? (
<DropDownMenuItem onClick={handleGenerateExecute}>Generate shell execute</DropDownMenuItem>
) : null
}
onLoad={data => {
newQuery({
title: file,
initialData: data,
// @ts-ignore
savedFile: file,
savedFolder: 'sql',
savedFormat: 'text',
});
}}
/>
);
}
export function SavedShellFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const openNewTab = useOpenNewTab();
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="text"
icon="img shell"
onLoad={data => {
openNewTab(
{
title: file,
icon: 'img shell',
tabComponent: 'ShellTab',
props: {
savedFile: file,
savedFolder: 'shell',
savedFormat: 'text',
},
},
{ editor: data }
);
}}
/>
);
}
export function SavedChartFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const openNewTab = useOpenNewTab();
const currentDatabase = useCurrentDatabase();
const connection = _.get(currentDatabase, 'connection') || {};
const database = _.get(currentDatabase, 'name');
const tooltip = `${connection.displayName || connection.server}\n${database}`;
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="json"
icon="img chart"
onLoad={data => {
openNewTab(
{
title: file,
icon: 'img chart',
tooltip,
props: {
conid: connection._id,
database,
savedFile: file,
savedFolder: 'charts',
savedFormat: 'json',
},
tabComponent: 'ChartTab',
},
{ editor: data }
);
}}
/>
);
}
export function SavedQueryFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const openNewTab = useOpenNewTab();
const currentDatabase = useCurrentDatabase();
const connection = _.get(currentDatabase, 'connection') || {};
const database = _.get(currentDatabase, 'name');
const tooltip = `${connection.displayName || connection.server}\n${database}`;
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="json"
icon="img query-design"
onLoad={data => {
openNewTab(
{
title: file,
icon: 'img query-design',
tooltip,
props: {
conid: connection._id,
database,
savedFile: file,
savedFolder: 'query',
savedFormat: 'json',
},
tabComponent: 'QueryDesignTab',
},
{ editor: data }
);
}}
/>
);
}
export function SavedMarkdownFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const openNewTab = useOpenNewTab();
const showPage = () => {
openNewTab({
title: file,
icon: 'img markdown',
tabComponent: 'MarkdownViewTab',
props: {
savedFile: file,
savedFolder: 'markdown',
savedFormat: 'text',
},
});
};
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="text"
icon="img markdown"
onLoad={data => {
openNewTab(
{
title: file,
icon: 'img markdown',
tabComponent: 'MarkdownEditorTab',
props: {
savedFile: file,
savedFolder: 'markdown',
savedFormat: 'text',
},
},
{ editor: data }
);
}}
menuExt={<DropDownMenuItem onClick={showPage}>Show page</DropDownMenuItem>}
/>
);
}
export function SavedFileAppObject({ data, commonProps }) {
const { folder } = data;
const folderTypes = {
sql: SavedSqlFileAppObject,
shell: SavedShellFileAppObject,
charts: SavedChartFileAppObject,
markdown: SavedMarkdownFileAppObject,
query: SavedQueryFileAppObject,
};
const AppObject = folderTypes[folder];
if (AppObject) {
return <AppObject data={data} commonProps={commonProps} />;
}
return null;
}
[
SavedSqlFileAppObject,
SavedShellFileAppObject,
SavedChartFileAppObject,
SavedMarkdownFileAppObject,
SavedFileAppObject,
].forEach(fn => {
// @ts-ignore
fn.extractKey = data => data.file;
});

View File

@@ -1,36 +0,0 @@
import { findForeignKeyForColumn } from 'dbgate-tools';
import React from 'react';
import { getColumnIcon } from '../datagrid/ColumnLabel';
import { AppObjectCore } from './AppObjectCore';
import { AppObjectList } from './AppObjectList';
function ColumnAppObject({ data, commonProps }) {
const { columnName, dataType, foreignKey } = data;
let extInfo = dataType;
if (foreignKey) extInfo += ` -> ${foreignKey.refTableName}`;
return (
<AppObjectCore
{...commonProps}
data={data}
title={columnName}
extInfo={extInfo}
icon={getColumnIcon(data, true)}
disableHover
/>
);
}
ColumnAppObject.extractKey = ({ columnName }) => columnName;
export default function SubColumnParamList({ data }) {
const { columns } = data;
return (
<AppObjectList
list={(columns || []).map(col => ({
...col,
foreignKey: findForeignKeyForColumn(data, col),
}))}
AppObjectComponent={ColumnAppObject}
/>
);
}

View File

@@ -1,99 +0,0 @@
import React from 'react';
import _ from 'lodash';
import { SelectField } from '../utility/inputs';
import ErrorInfo from '../widgets/ErrorInfo';
import styled from 'styled-components';
import { TextCellViewWrap, TextCellViewNoWrap } from './TextCellView';
import JsonCellView from './JsonCellDataView';
import useTheme from '../theme/useTheme';
const Toolbar = styled.div`
display: flex;
background: ${props => props.theme.toolbar_background};
align-items: center;
`;
const MainWrapper = styled.div`
display: flex;
flex: 1;
flex-direction: column;
`;
const DataWrapper = styled.div`
display: flex;
flex: 1;
`;
const formats = [
{
type: 'textWrap',
title: 'Text (wrap)',
Component: TextCellViewWrap,
single: true,
},
{
type: 'text',
title: 'Text (no wrap)',
Component: TextCellViewNoWrap,
single: true,
},
{
type: 'json',
title: 'Json',
Component: JsonCellView,
single: true,
},
];
function autodetect(selection, grider, value) {
if (_.isString(value)) {
if (value.startsWith('[') || value.startsWith('{')) return 'json';
}
return 'textWrap';
}
export default function CellDataView({ selection = undefined, grider = undefined, selectedValue = undefined }) {
const [selectedFormatType, setSelectedFormatType] = React.useState('autodetect');
const theme = useTheme();
let value = null;
if (grider && selection && selection.length == 1) {
const rowData = grider.getRowData(selection[0].row);
const { column } = selection[0];
if (rowData) value = rowData[column];
}
if (selectedValue) {
value = selectedValue;
}
const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]);
const autodetectFormat = formats.find(x => x.type == autodetectFormatType);
const usedFormatType = selectedFormatType == 'autodetect' ? autodetectFormatType : selectedFormatType;
const usedFormat = formats.find(x => x.type == usedFormatType);
const { Component } = usedFormat || {};
return (
<MainWrapper>
<Toolbar theme={theme}>
Format:
<SelectField value={selectedFormatType} onChange={e => setSelectedFormatType(e.target.value)}>
<option value="autodetect">Autodetect - {autodetectFormat.title}</option>
{formats.map(fmt => (
<option value={fmt.type} key={fmt.type}>
{fmt.title}
</option>
))}
</SelectField>
</Toolbar>
<DataWrapper>
{usedFormat == null || (usedFormat.single && value == null) ? (
<ErrorInfo message="Must be selected one cell" />
) : (
<Component value={value} grider={grider} selection={selection} />
)}
</DataWrapper>
</MainWrapper>
);
}

View File

@@ -1,35 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import ReactJson from 'react-json-view';
import ErrorInfo from '../widgets/ErrorInfo';
import useTheme from '../theme/useTheme';
const OuterWrapper = styled.div`
flex: 1;
position: relative;
`;
const InnerWrapper = styled.div`
overflow: scroll;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
`;
export default function JsonCellView({ value }) {
const theme = useTheme();
try {
const json = React.useMemo(() => JSON.parse(value), [value]);
return (
<OuterWrapper>
<InnerWrapper>
<ReactJson src={json} theme={theme.jsonViewerTheme} />
</InnerWrapper>
</OuterWrapper>
);
} catch (err) {
return <ErrorInfo message="Error parsing JSON" />;
}
}

View File

@@ -1,22 +0,0 @@
import React from 'react';
import styled from 'styled-components';
const StyledInput = styled.textarea`
flex: 1;
`;
export function TextCellViewWrap({ value, grider, selection }) {
return <StyledInput value={value} wrap="hard" readOnly />;
}
export function TextCellViewNoWrap({ value, grider, selection }) {
return (
<StyledInput
value={value}
wrap="off"
readOnly
// readOnly={grider ? !grider.editable : true}
// onChange={(e) => grider.setCellValue(selection[0].row, selection[0].column, e.target.value)}
/>
);
}

View File

@@ -1,154 +0,0 @@
import React from 'react';
import Chart from 'react-chartjs-2';
import _ from 'lodash';
import styled from 'styled-components';
import useTheme from '../theme/useTheme';
import useDimensions from '../utility/useDimensions';
import { HorizontalSplitter } from '../widgets/Splitter';
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
import { FormCheckboxField, FormSelectField, FormTextField } from '../utility/forms';
import DataChart from './DataChart';
import { FormProviderCore } from '../utility/FormProvider';
import { loadChartData, loadChartStructure } from './chartDataLoader';
import useExtensions from '../utility/useExtensions';
import { getConnectionInfo } from '../utility/metadataLoaders';
import { findEngineDriver } from 'dbgate-tools';
import { FormFieldTemplateTiny } from '../utility/formStyle';
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
import { presetPrimaryColors } from '@ant-design/colors';
import ErrorInfo from '../widgets/ErrorInfo';
const LeftContainer = styled.div`
background-color: ${props => props.theme.manager_background};
display: flex;
flex: 1;
`;
export default function ChartEditor({ data, config, setConfig, sql, conid, database }) {
const [managerSize, setManagerSize] = React.useState(0);
const theme = useTheme();
const extensions = useExtensions();
const [error, setError] = React.useState(null);
const [availableColumnNames, setAvailableColumnNames] = React.useState([]);
const [loadedData, setLoadedData] = React.useState(null);
const getDriver = async () => {
const conn = await getConnectionInfo({ conid });
if (!conn) return;
const driver = findEngineDriver(conn, extensions);
return driver;
};
const handleLoadColumns = async () => {
const driver = await getDriver();
if (!driver) return;
try {
const columns = await loadChartStructure(driver, conid, database, sql);
setAvailableColumnNames(columns);
} catch (err) {
setError(err.message);
}
};
const handleLoadData = async () => {
const driver = await getDriver();
if (!driver) return;
const loaded = await loadChartData(driver, conid, database, sql, config);
if (!loaded) return;
const { columns, rows } = loaded;
setLoadedData({
structure: columns,
rows,
});
};
React.useEffect(() => {
if (sql && conid && database) {
handleLoadColumns();
}
}, [sql, conid, database, extensions]);
React.useEffect(() => {
if (data) {
setAvailableColumnNames(data ? data.structure.columns.map(x => x.columnName) : []);
}
}, [data]);
React.useEffect(() => {
if (config.labelColumn && sql && conid && database) {
handleLoadData();
}
}, [config, sql, conid, database, availableColumnNames]);
if (error) {
return (
<div>
<ErrorInfo message={error} />
</div>
);
}
return (
<FormProviderCore values={config} setValues={setConfig} template={FormFieldTemplateTiny}>
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
<LeftContainer theme={theme}>
<WidgetColumnBar>
<WidgetColumnBarItem title="Style" name="style" height="40%">
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
<FormSelectField label="Chart type" name="chartType">
<option value="bar">Bar</option>
<option value="line">Line</option>
{/* <option value="radar">Radar</option> */}
<option value="pie">Pie</option>
<option value="polarArea">Polar area</option>
{/* <option value="bubble">Bubble</option>
<option value="scatter">Scatter</option> */}
</FormSelectField>
<FormTextField label="Color set" name="colorSeed" />
<FormSelectField label="Truncate from" name="truncateFrom">
<option value="begin">Begin</option>
<option value="end">End (most recent data for datetime)</option>
</FormSelectField>
<FormTextField label="Truncate limit" name="truncateLimit" />
<FormCheckboxField label="Show relative values" name="showRelativeValues" />
</ManagerInnerContainer>
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Data" name="data">
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
{availableColumnNames.length > 0 && (
<FormSelectField label="Label column" name="labelColumn">
<option value=""></option>
{availableColumnNames.map(col => (
<option value={col} key={col}>
{col}
</option>
))}
</FormSelectField>
)}
{availableColumnNames.map(col => (
<React.Fragment key={col}>
<FormCheckboxField label={col} name={`dataColumn_${col}`} />
{config[`dataColumn_${col}`] && (
<FormSelectField label="Color" name={`dataColumnColor_${col}`}>
<option value="">Random</option>
{_.keys(presetPrimaryColors).map(color => (
<option value={color} key={color}>
{_.startCase(color)}
</option>
))}
</FormSelectField>
)}
</React.Fragment>
))}
</ManagerInnerContainer>
</WidgetColumnBarItem>
</WidgetColumnBar>
</LeftContainer>
<DataChart data={data || loadedData} />
</HorizontalSplitter>
</FormProviderCore>
);
}

View File

@@ -1,15 +0,0 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
export default function ChartToolbar({ modelState, dispatchModel }) {
return (
<>
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo
</ToolbarButton>
<ToolbarButton disabled={!modelState.canRedo} onClick={() => dispatchModel({ type: 'redo' })} icon="icon redo">
Redo
</ToolbarButton>
</>
);
}

View File

@@ -1,165 +0,0 @@
import React from 'react';
import _ from 'lodash';
import Chart from 'react-chartjs-2';
import randomcolor from 'randomcolor';
import styled from 'styled-components';
import useDimensions from '../utility/useDimensions';
import { useForm } from '../utility/FormProvider';
import useTheme from '../theme/useTheme';
import moment from 'moment';
const ChartWrapper = styled.div`
flex: 1;
overflow: hidden;
`;
function getTimeAxis(labels) {
const res = [];
for (const label of labels) {
const parsed = moment(label);
if (!parsed.isValid()) return null;
const iso = parsed.toISOString();
if (iso < '1850-01-01T00:00:00' || iso > '2150-01-01T00:00:00') return null;
res.push(parsed);
}
return res;
}
function getLabels(labelValues, timeAxis, chartType) {
if (!timeAxis) return labelValues;
if (chartType === 'line') return timeAxis.map(x => x.toDate());
return timeAxis.map(x => x.format('D. M. YYYY'));
}
function getOptions(timeAxis, chartType) {
if (timeAxis && chartType === 'line') {
return {
scales: {
xAxes: [
{
type: 'time',
distribution: 'linear',
time: {
tooltipFormat: 'D. M. YYYY HH:mm',
displayFormats: {
millisecond: 'HH:mm:ss.SSS',
second: 'HH:mm:ss',
minute: 'HH:mm',
hour: 'D.M hA',
day: 'D. M.',
week: 'D. M. YYYY',
month: 'MM-YYYY',
quarter: '[Q]Q - YYYY',
year: 'YYYY',
},
},
},
],
},
};
}
return {};
}
function createChartData(freeData, labelColumn, dataColumns, colorSeed, chartType, dataColumnColors, theme) {
if (!freeData || !labelColumn || !dataColumns || !freeData.rows || dataColumns.length == 0) return [{}, {}];
const colors = randomcolor({
count: _.max([freeData.rows.length, dataColumns.length, 1]),
seed: colorSeed,
});
let backgroundColor = null;
let borderColor = null;
const labelValues = freeData.rows.map(x => x[labelColumn]);
const timeAxis = getTimeAxis(labelValues);
const labels = getLabels(labelValues, timeAxis, chartType);
const res = {
labels,
datasets: dataColumns.map((dataColumn, columnIndex) => {
if (chartType == 'line' || chartType == 'bar') {
const color = dataColumnColors[dataColumn];
if (color) {
backgroundColor = theme.main_palettes[color][4] + '80';
borderColor = theme.main_palettes[color][7];
} else {
backgroundColor = colors[columnIndex] + '80';
borderColor = colors[columnIndex];
}
} else {
backgroundColor = colors;
}
return {
label: dataColumn,
data: freeData.rows.map(row => row[dataColumn]),
backgroundColor,
borderColor,
borderWidth: 1,
};
}),
};
const options = getOptions(timeAxis, chartType);
return [res, options];
}
export function extractDataColumns(values) {
const dataColumns = [];
for (const key in values) {
if (key.startsWith('dataColumn_') && values[key]) {
dataColumns.push(key.substring('dataColumn_'.length));
}
}
return dataColumns;
}
export function extractDataColumnColors(values, dataColumns) {
const res = {};
for (const column of dataColumns) {
const color = values[`dataColumnColor_${column}`];
if (color) res[column] = color;
}
return res;
}
export default function DataChart({ data }) {
const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions();
const { values } = useForm();
const theme = useTheme();
const { labelColumn } = values;
const dataColumns = extractDataColumns(values);
const dataColumnColors = extractDataColumnColors(values, dataColumns);
const [chartData, options] = createChartData(
data,
labelColumn,
dataColumns,
values.colorSeed || '5',
values.chartType,
dataColumnColors,
theme
);
return (
<ChartWrapper ref={containerRef}>
<Chart
key={`${values.chartType}|${containerWidth}|${containerHeight}`}
width={containerWidth}
height={containerHeight}
data={chartData}
type={values.chartType}
options={{
...options,
// elements: {
// point: {
// radius: 0,
// },
// },
// tooltips: {
// mode: 'index',
// intersect: false,
// },
}}
/>
</ChartWrapper>
);
}

View File

@@ -1,105 +0,0 @@
import { dumpSqlSelect, Select } from 'dbgate-sqltree';
import { EngineDriver } from 'dbgate-types';
import axios from '../utility/axios';
import _ from 'lodash';
import { extractDataColumns } from './DataChart';
export async function loadChartStructure(driver: EngineDriver, conid, database, sql) {
const select: Select = {
commandType: 'select',
selectAll: true,
topRecords: 1,
from: {
subQueryString: sql,
alias: 'subq',
},
};
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
if (resp.data.errorMessage) throw new Error(resp.data.errorMessage);
return resp.data.columns.map(x => x.columnName);
}
export async function loadChartData(driver: EngineDriver, conid, database, sql, config) {
const dataColumns = extractDataColumns(config);
const { labelColumn, truncateFrom, truncateLimit, showRelativeValues } = config;
if (!labelColumn || !dataColumns || dataColumns.length == 0) return null;
const select: Select = {
commandType: 'select',
columns: [
{
exprType: 'column',
source: { alias: 'subq' },
columnName: labelColumn,
alias: labelColumn,
},
// @ts-ignore
...dataColumns.map(columnName => ({
exprType: 'call',
func: 'SUM',
args: [
{
exprType: 'column',
columnName,
source: { alias: 'subq' },
},
],
alias: columnName,
})),
],
topRecords: truncateLimit || 100,
from: {
subQueryString: sql,
alias: 'subq',
},
groupBy: [
{
exprType: 'column',
source: { alias: 'subq' },
columnName: labelColumn,
},
],
orderBy: [
{
exprType: 'column',
source: { alias: 'subq' },
columnName: labelColumn,
direction: truncateFrom == 'end' ? 'DESC' : 'ASC',
},
],
};
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
let { rows, columns } = resp.data;
if (truncateFrom == 'end' && rows) {
rows = _.reverse([...rows]);
}
if (showRelativeValues) {
const maxValues = dataColumns.map(col => _.max(rows.map(row => row[col])));
for (const [col, max] of _.zip(dataColumns, maxValues)) {
if (!max) continue;
if (!_.isNumber(max)) continue;
if (!(max > 0)) continue;
rows = rows.map(row => ({
...row,
[col]: (row[col] / max) * 100,
}));
// columns = columns.map((x) => {
// if (x.columnName == col) {
// return { columnName: `${col} %` };
// }
// return x;
// });
}
}
return {
columns,
rows,
};
}

View File

@@ -1,164 +0,0 @@
import {
ChangeSet,
changeSetContainsChanges,
changeSetInsertNewRow,
createChangeSet,
deleteChangeSetRows,
findExistingChangeSetItem,
getChangeSetInsertedRows,
GridDisplay,
revertChangeSetRowChanges,
setChangeSetValue,
} from 'dbgate-datalib';
import Grider, { GriderRowStatus } from './Grider';
export default class ChangeSetGrider extends Grider {
public insertedRows: any[];
public changeSet: ChangeSet;
public setChangeSet: Function;
private rowCacheIndexes: Set<number>;
private rowDataCache;
private rowStatusCache;
private rowDefinitionsCache;
private batchChangeSet: ChangeSet;
constructor(public sourceRows: any[], public changeSetState, public dispatchChangeSet, public display: GridDisplay) {
super();
this.changeSet = changeSetState && changeSetState.value;
this.insertedRows = getChangeSetInsertedRows(this.changeSet, display.baseTable);
this.setChangeSet = value => dispatchChangeSet({ type: 'set', value });
this.rowCacheIndexes = new Set();
this.rowDataCache = {};
this.rowStatusCache = {};
this.rowDefinitionsCache = {};
this.batchChangeSet = null;
}
getRowSource(index: number) {
if (index < this.sourceRows.length) return this.sourceRows[index];
return null;
}
getInsertedRowIndex(index) {
return index >= this.sourceRows.length ? index - this.sourceRows.length : null;
}
requireRowCache(index: number) {
if (this.rowCacheIndexes.has(index)) return;
const row = this.getRowSource(index);
const insertedRowIndex = this.getInsertedRowIndex(index);
const rowDefinition = this.display.getChangeSetRow(row, insertedRowIndex);
const [matchedField, matchedChangeSetItem] = findExistingChangeSetItem(this.changeSet, rowDefinition);
const rowUpdated = matchedChangeSetItem ? { ...row, ...matchedChangeSetItem.fields } : row;
let status = 'regular';
if (matchedChangeSetItem && matchedField == 'updates') status = 'updated';
if (matchedField == 'deletes') status = 'deleted';
if (insertedRowIndex != null) status = 'inserted';
const rowStatus = {
status,
modifiedFields:
matchedChangeSetItem && matchedChangeSetItem.fields ? new Set(Object.keys(matchedChangeSetItem.fields)) : null,
};
this.rowDataCache[index] = rowUpdated;
this.rowStatusCache[index] = rowStatus;
this.rowDefinitionsCache[index] = rowDefinition;
this.rowCacheIndexes.add(index);
}
get editable() {
return this.display.editable;
}
get canInsert() {
return !!this.display.baseTable;
}
getRowData(index: number) {
this.requireRowCache(index);
return this.rowDataCache[index];
}
getRowStatus(index): GriderRowStatus {
this.requireRowCache(index);
return this.rowStatusCache[index];
}
get rowCount() {
return this.sourceRows.length + this.insertedRows.length;
}
applyModification(changeSetReducer) {
if (this.batchChangeSet) {
this.batchChangeSet = changeSetReducer(this.batchChangeSet);
} else {
this.setChangeSet(changeSetReducer(this.changeSet));
}
}
setCellValue(index: number, uniqueName: string, value: any) {
const row = this.getRowSource(index);
const definition = this.display.getChangeSetField(row, uniqueName, this.getInsertedRowIndex(index));
this.applyModification(chs => setChangeSetValue(chs, definition, value));
}
deleteRow(index: number) {
this.requireRowCache(index);
this.applyModification(chs => deleteChangeSetRows(chs, this.rowDefinitionsCache[index]));
}
get rowCountInUpdate() {
if (this.batchChangeSet) {
const newRows = getChangeSetInsertedRows(this.batchChangeSet, this.display.baseTable);
return this.sourceRows.length + newRows.length;
} else {
return this.rowCount;
}
}
insertRow(): number {
const res = this.rowCountInUpdate;
this.applyModification(chs => changeSetInsertNewRow(chs, this.display.baseTable));
return res;
}
beginUpdate() {
this.batchChangeSet = this.changeSet;
}
endUpdate() {
this.setChangeSet(this.batchChangeSet);
this.batchChangeSet = null;
}
revertRowChanges(index: number) {
this.requireRowCache(index);
this.applyModification(chs => revertChangeSetRowChanges(chs, this.rowDefinitionsCache[index]));
}
revertAllChanges() {
this.applyModification(chs => createChangeSet());
}
undo() {
this.dispatchChangeSet({ type: 'undo' });
}
redo() {
this.dispatchChangeSet({ type: 'redo' });
}
get canUndo() {
return this.changeSetState.canUndo;
}
get canRedo() {
return this.changeSetState.canRedo;
}
get containsChanges() {
return changeSetContainsChanges(this.changeSet);
}
get disableLoadNextPage() {
return this.insertedRows.length > 0;
}
static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
}
static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
}
}

View File

@@ -1,134 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import ColumnLabel from './ColumnLabel';
import DropDownButton from '../widgets/DropDownButton';
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
import { useSplitterDrag } from '../widgets/Splitter';
import { isTypeDateTime } from 'dbgate-tools';
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject';
import { useSetOpenedTabs } from '../utility/globalState';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import useOpenNewTab from '../utility/useOpenNewTab';
const HeaderDiv = styled.div`
display: flex;
flex-wrap: nowrap;
`;
const LabelDiv = styled.div`
flex: 1;
min-width: 10px;
// padding-left: 2px;
padding: 2px;
margin: auto;
white-space: nowrap;
`;
const IconWrapper = styled.span`
margin-left: 3px;
`;
const ResizeHandle = styled.div`
background-color: ${props => props.theme.border};
width: 2px;
cursor: col-resize;
z-index: 1;
`;
const GroupingLabel = styled.span`
color: green;
white-space: nowrap;
`;
export default function ColumnHeaderControl({
column,
setSort,
onResize,
order,
setGrouping,
grouping,
conid,
database,
}) {
const onResizeDown = useSplitterDrag('clientX', onResize);
const { foreignKey } = column;
const openNewTab = useOpenNewTab();
const theme = useTheme();
const openReferencedTable = () => {
openDatabaseObjectDetail(openNewTab, 'TableDataTab', null, {
schemaName: foreignKey.refSchemaName,
pureName: foreignKey.refTableName,
conid,
database,
objectTypeField: 'tables',
});
// openNewTab(setOpenedTabs, {
// title: foreignKey.refTableName,
// tooltip,
// icon: sqlTemplate ? 'sql.svg' : icons[objectTypeField],
// tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
// props: {
// schemaName,
// pureName,
// conid,
// database,
// objectTypeField,
// initialArgs: sqlTemplate ? { sqlTemplate } : null,
// },
// });
};
return (
<HeaderDiv>
<LabelDiv>
{grouping && (
<GroupingLabel>{grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}:</GroupingLabel>
)}
<ColumnLabel {...column} />
{order == 'ASC' && (
<IconWrapper>
<FontIcon icon="img sort-asc" />
</IconWrapper>
)}
{order == 'DESC' && (
<IconWrapper>
<FontIcon icon="img sort-desc" />
</IconWrapper>
)}
</LabelDiv>
{setSort && (
<DropDownButton>
<DropDownMenuItem onClick={() => setSort('ASC')}>Sort ascending</DropDownMenuItem>
<DropDownMenuItem onClick={() => setSort('DESC')}>Sort descending</DropDownMenuItem>
<DropDownMenuDivider />
{foreignKey && (
<DropDownMenuItem onClick={openReferencedTable}>
Open table <strong>{foreignKey.refTableName}</strong>
</DropDownMenuItem>
)}
{foreignKey && <DropDownMenuDivider />}
<DropDownMenuItem onClick={() => setGrouping('GROUP')}>Group by</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('MAX')}>MAX</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('MIN')}>MIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('SUM')}>SUM</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('AVG')}>AVG</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('COUNT')}>COUNT</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('COUNT DISTINCT')}>COUNT DISTINCT</DropDownMenuItem>
{isTypeDateTime(column.dataType) && (
<>
<DropDownMenuDivider />
<DropDownMenuItem onClick={() => setGrouping('GROUP:YEAR')}>Group by YEAR</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('GROUP:MONTH')}>Group by MONTH</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('GROUP:DAY')}>Group by DAY</DropDownMenuItem>
{/* <DropDownMenuItem onClick={() => setGrouping('GROUP:HOUR')}>Group by HOUR</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('GROUP:MINUTE')}>Group by MINUTE</DropDownMenuItem> */}
</>
)}
</DropDownButton>
)}
<ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} theme={theme} />
</HeaderDiv>
);
}

View File

@@ -1,35 +0,0 @@
//@ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
const Label = styled.span`
font-weight: ${props => (props.notNull ? 'bold' : 'normal')};
white-space: nowrap;
`;
const ExtInfoWrap = styled.span`
font-weight: normal;
margin-left: 5px;
color: ${props => props.theme.left_font3};
`;
export function getColumnIcon(column, forceIcon = false) {
if (column.autoIncrement) return 'img autoincrement';
if (column.foreignKey) return 'img foreign-key';
if (forceIcon) return 'img column';
return null;
}
/** @param column {import('dbgate-datalib').DisplayColumn|import('dbgate-types').ColumnInfo} */
export default function ColumnLabel(column) {
const icon = getColumnIcon(column, column.forceIcon);
const theme = useTheme();
return (
<Label {...column}>
{icon ? <FontIcon icon={icon} /> : null} {column.headerText || column.columnName}
{column.extInfo ? <ExtInfoWrap theme={theme}>{column.extInfo}</ExtInfoWrap> : null}
</Label>
);
}

View File

@@ -1,94 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import ColumnLabel from './ColumnLabel';
import { filterName } from 'dbgate-datalib';
import { ExpandIcon } from '../icons';
import InlineButton from '../widgets/InlineButton';
import { ManagerInnerContainer } from './ManagerStyles';
import SearchInput from '../widgets/SearchInput';
import useTheme from '../theme/useTheme';
const Wrapper = styled.div``;
const Row = styled.div`
margin-left: 5px;
margin-right: 5px;
cursor: pointer;
white-space: nowrap;
&:hover {
background-color: ${props => props.theme.manager_background_blue[1]};
}
`;
const SearchBoxWrapper = styled.div`
display: flex;
margin-bottom: 5px;
`;
const Button = styled.button`
// -webkit-appearance: none;
// -moz-appearance: none;
// appearance: none;
// width: 50px;
`;
/**
* @param {object} props
* @param {import('dbgate-datalib').GridDisplay} props.display
* @param {import('dbgate-datalib').DisplayColumn} props.column
*/
function ColumnManagerRow(props) {
const { display, column } = props;
const [isHover, setIsHover] = React.useState(false);
const theme = useTheme();
return (
<Row
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
theme={theme}
onClick={e => {
// @ts-ignore
if (e.target.closest('.expandColumnIcon')) return;
display.focusColumn(column.uniqueName);
}}
>
<ExpandIcon
className="expandColumnIcon"
isBlank={!column.foreignKey}
isExpanded={column.foreignKey && display.isExpandedColumn(column.uniqueName)}
onClick={() => display.toggleExpandedColumn(column.uniqueName)}
/>
<input
type="checkbox"
style={{ marginLeft: `${5 + (column.uniquePath.length - 1) * 10}px` }}
checked={column.isChecked}
onChange={() => display.setColumnVisibility(column.uniquePath, !column.isChecked)}
></input>
<ColumnLabel {...column} />
</Row>
);
}
/** @param props {import('./types').DataGridProps} */
export default function ColumnManager(props) {
const { display } = props;
const [columnFilter, setColumnFilter] = React.useState('');
return (
<>
<SearchBoxWrapper>
<SearchInput placeholder="Search columns" filter={columnFilter} setFilter={setColumnFilter} />
<InlineButton onClick={() => display.hideAllColumns()}>Hide</InlineButton>
<InlineButton onClick={() => display.showAllColumns()}>Show</InlineButton>
</SearchBoxWrapper>
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
{display
.getColumns(columnFilter)
.filter(column => filterName(columnFilter, column.columnName))
.map(column => (
<ColumnManagerRow key={column.uniqueName} display={display} column={column} />
))}
</ManagerInnerContainer>
</>
);
}

View File

@@ -1,501 +0,0 @@
// @ts-nocheck
import React from 'react';
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
import styled from 'styled-components';
import keycodes from '../utility/keycodes';
import { parseFilter, createMultiLineFilter } from 'dbgate-filterparser';
import InlineButton from '../widgets/InlineButton';
import useShowModal from '../modals/showModal';
import FilterMultipleValuesModal from '../modals/FilterMultipleValuesModal';
import SetFilterModal from '../modals/SetFilterModal';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import { useShowMenu } from '../modals/showMenu';
// import { $ } from '../../Utility/jquery';
// import autobind from 'autobind-decorator';
// import * as React from 'react';
// import { createMultiLineFilter } from '../../DataLib/FilterTools';
// import { ModalDialog } from '../Dialogs';
// import { FilterDialog } from '../Dialogs/FilterDialog';
// import { FilterMultipleValuesDialog } from '../Dialogs/FilterMultipleValuesDialog';
// import { IconSpan } from '../Navigation/NavUtils';
// import { KeyCodes } from '../ReactDataGrid/KeyCodes';
// import { DropDownMenu, DropDownMenuDivider, DropDownMenuItem, DropDownSubmenuItem } from './DropDownMenu';
// import { FilterParserType } from '../../SwaggerClients';
// import { IFilterHolder } from '../CommonControls';
// import { GrayFilterIcon } from '../Icons';
// export interface IDataFilterControlProps {
// filterType: FilterParserType;
// getFilter: Function;
// setFilter: Function;
// width: number;
// onControlKey?: Function;
// isReadOnly?: boolean;
// inputElementId?: string;
// }
const FilterDiv = styled.div`
display: flex;
`;
const FilterInput = styled.input`
flex: 1;
min-width: 10px;
background-color: ${props =>
props.state == 'ok'
? props.theme.input_background_green[1]
: props.state == 'error'
? props.theme.input_background_red[1]
: props.theme.input_background};
`;
// const FilterButton = styled.button`
// color: gray;
// `;
function DropDownContent({ filterType, setFilter, filterMultipleValues, openFilterWindow }) {
switch (filterType) {
case 'number':
return (
<>
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('=')}>Equals...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('>')}>Greater Than...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('>=')}>Greater Than Or Equal To...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('<')}>Less Than...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('<=')}>Less Than Or Equal To...</DropDownMenuItem>
</>
);
case 'logical':
return (
<>
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('TRUE')}>Is True</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('FALSE')}>Is False</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('TRUE, NULL')}>Is True or NULL</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('FALSE, NULL')}>Is False or NULL</DropDownMenuItem>
</>
);
case 'datetime':
return (
<>
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => openFilterWindow('<=')}>Before...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('>=')}>After...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('>=;<=')}>Between...</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('TOMORROW')}>Tomorrow</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('TODAY')}>Today</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('YESTERDAY')}>Yesterday</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('NEXT WEEK')}>Next Week</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('THIS WEEK')}>This Week</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('LAST WEEK')}>Last Week</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('NEXT MONTH')}>Next Month</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('THIS MONTH')}>This Month</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('LAST MONTH')}>Last Month</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('NEXT YEAR')}>Next Year</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('THIS YEAR')}>This Year</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('LAST YEAR')}>Last Year</DropDownMenuItem>
<DropDownMenuDivider />
{/* <DropDownSubmenuItem title="All dates in period">
<DropDownMenuItem onClick={x => setFilter('JAN')}>January</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('FEB')}>February</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('MAR')}>March</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('APR')}>April</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('JUN')}>June</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('JUL')}>July</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('AUG')}>August</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('SEP')}>September</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('OCT')}>October</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOV')}>November</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('DEC')}>December</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('MON')}>Monday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('TUE')}>Tuesday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('WED')}>Wednesday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('THU')}>Thursday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('FRI')}>Friday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('SAT')}>Saturday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('SUN')}>Sunday</DropDownMenuItem>
</DropDownSubmenuItem> */}
</>
);
case 'string':
return (
<>
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('=')}>Equals...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('EMPTY, NULL')}>Is Empty Or Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => openFilterWindow('+')}>Contains...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('~')}>Does Not Contain...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('^')}>Begins With...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('!^')}>Does Not Begin With...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('$')}>Ends With...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('!$')}>Does Not End With...</DropDownMenuItem>
</>
);
}
}
export default function DataFilterControl({
isReadOnly = false,
filterType,
filter,
setFilter,
focusIndex = 0,
onFocusGrid = undefined,
}) {
const showModal = useShowModal();
const showMenu = useShowMenu();
const theme = useTheme();
const [filterState, setFilterState] = React.useState('empty');
const setFilterText = filter => {
setFilter(filter);
editorRef.current.value = filter || '';
updateFilterState();
};
const applyFilter = () => {
if ((filter || '') == (editorRef.current.value || '')) return;
setFilter(editorRef.current.value);
};
const filterMultipleValues = () => {
showModal(modalState => (
<FilterMultipleValuesModal
modalState={modalState}
onFilter={(mode, text) => setFilterText(createMultiLineFilter(mode, text))}
/>
));
};
const openFilterWindow = operator => {
showModal(modalState => (
<SetFilterModal
filterType={filterType}
modalState={modalState}
onFilter={text => setFilterText(text)}
condition1={operator}
/>
));
};
const buttonRef = React.useRef();
const editorRef = React.useRef();
React.useEffect(() => {
if (focusIndex) editorRef.current.focus();
}, [focusIndex]);
const handleKeyDown = ev => {
if (isReadOnly) return;
if (ev.keyCode == keycodes.enter) {
applyFilter();
}
if (ev.keyCode == keycodes.escape) {
setFilterText('');
}
if (ev.keyCode == keycodes.downArrow) {
if (onFocusGrid) onFocusGrid();
// ev.stopPropagation();
ev.preventDefault();
}
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
// }
};
const updateFilterState = () => {
const value = editorRef.current.value;
try {
if (value) {
parseFilter(value, filterType);
setFilterState('ok');
} else {
setFilterState('empty');
}
} catch (err) {
// console.log('PARSE ERROR', err);
setFilterState('error');
}
};
React.useEffect(() => {
editorRef.current.value = filter || '';
updateFilterState();
}, [filter]);
const handleShowMenu = () => {
const rect = buttonRef.current.getBoundingClientRect();
showMenu(
rect.left,
rect.bottom,
<DropDownContent
filterType={filterType}
setFilter={setFilterText}
filterMultipleValues={filterMultipleValues}
openFilterWindow={openFilterWindow}
/>
);
};
function handlePaste(event) {
var pastedText = undefined;
// @ts-ignore
if (window.clipboardData && window.clipboardData.getData) {
// IE
// @ts-ignore
pastedText = window.clipboardData.getData('Text');
} else if (event.clipboardData && event.clipboardData.getData) {
pastedText = event.clipboardData.getData('text/plain');
}
if (pastedText && pastedText.includes('\n')) {
event.preventDefault();
setFilterText(createMultiLineFilter('is', pastedText));
}
}
return (
<FilterDiv>
<FilterInput
theme={theme}
ref={editorRef}
onKeyDown={handleKeyDown}
type="text"
readOnly={isReadOnly}
onChange={updateFilterState}
state={filterState}
onBlur={applyFilter}
onPaste={handlePaste}
autocomplete="off"
/>
<InlineButton buttonRef={buttonRef} onClick={handleShowMenu} square>
<FontIcon icon="icon filter" />
</InlineButton>
</FilterDiv>
);
}
// domEditor: Element;
// @autobind
// applyFilter() {
// this.props.setFilter($(this.domEditor).val());
// }
// @autobind
// clearFilter() {
// $(this.domEditor).val('');
// this.applyFilter();
// }
// setFilter(value: string) {
// $(this.domEditor).val(value);
// this.applyFilter();
// return false;
// }
// render() {
// let dropDownContent = null;
// let filterIconSpan = <span className='fa fa-filter' style={{color: 'gray', display: 'inline-block', width: '8px', height: '0', whiteSpace: 'nowrap'}} ></span>;
// //filterIconSpan = null;
// if (this.props.filterType == 'Number') {
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('=')}>Equals...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('>')}>Greater Than...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=')}>Greater Than Or Equal To...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('<')}>Less Than...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('<=')}>Less Than Or Equal To...</DropDownMenuItem>
// </DropDownMenu>;
// }
// if (this.props.filterType == 'Logical') {
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('TRUE')}>Is True</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('FALSE')}>Is False</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('TRUE, NULL')}>Is True or NULL</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('FALSE, NULL')}>Is False or NULL</DropDownMenuItem>
// </DropDownMenu>;
// }
// if (this.props.filterType == 'DateTime') {
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.openFilterWindow('<=')}>Before...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=')}>After...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=;<=')}>Between...</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('TOMORROW')}>Tomorrow</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('TODAY')}>Today</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('YESTERDAY')}>Yesterday</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('NEXT WEEK')}>Next Week</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('THIS WEEK')}>This Week</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('LAST WEEK')}>Last Week</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('NEXT MONTH')}>Next Month</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('THIS MONTH')}>This Month</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('LAST MONTH')}>Last Month</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('NEXT YEAR')}>Next Year</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('THIS YEAR')}>This Year</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('LAST YEAR')}>Last Year</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownSubmenuItem title='All dates in period'>
// <DropDownMenuItem onClick={x => this.setFilter('JAN')}>January</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('FEB')}>February</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('MAR')}>March</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('APR')}>April</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('JUN')}>June</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('JUL')}>July</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('AUG')}>August</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('SEP')}>September</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('OCT')}>October</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOV')}>November</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('DEC')}>December</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('MON')}>Monday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('TUE')}>Tuesday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('WED')}>Wednesday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('THU')}>Thursday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('FRI')}>Friday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('SAT')}>Saturday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('SUN')}>Sunday</DropDownMenuItem>
// </DropDownSubmenuItem>
// </DropDownMenu>;
// }
// if (this.props.filterType == 'String') {
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('=')}>Equals...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('EMPTY, NULL')}>Is Empty Or Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.openFilterWindow('+')}>Contains...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('~')}>Does Not Contain...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('^')}>Begins With...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('!^')}>Does Not Begin With...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('$')}>Ends With...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('!$')}>Does Not End With...</DropDownMenuItem>
// </DropDownMenu>;
// }
// if (this.props.isReadOnly) {
// dropDownContent = <GrayFilterIcon style={{marginLeft: '5px'}} />;
// }
// return <div style={{ minWidth: `${this.props.width}px`, maxWidth: `${this.props.width}px`, width: `${this.props.width}` }}>
// <input id={this.props.inputElementId} type='text' style={{ 'width': `${(this.props.width - 20)}px` }} readOnly={this.props.isReadOnly}
// onBlur={this.applyFilter} ref={x => this.setDomEditor(x)} onKeyDown={this.editorKeyDown} placeholder='Search' ></input>
// {dropDownContent}
// </div>;
// }
// async filterMultipleValues() {
// let result = await ModalDialog.run(<FilterMultipleValuesDialog header='Filter multiple values' />);
// if (!result) return;
// let { mode, text } = result;
// let filter = createMultiLineFilter(mode, text);
// this.setFilter(filter);
// }
// openFilterWindow(selectedOperator: string) {
// FilterDialog.runFilter(this, this.props.filterType, selectedOperator);
// return false;
// }
// setDomEditor(editor) {
// this.domEditor = editor;
// $(editor).val(this.props.getFilter());
// }
// @autobind
// editorKeyDown(ev) {
// if (this.props.isReadOnly) return;
// if (ev.keyCode == KeyCodes.Enter) {
// this.applyFilter();
// }
// if (ev.keyCode == KeyCodes.Escape) {
// this.clearFilter();
// }
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
// }
// }
// focus() {
// $(this.domEditor).focus();
// }
// }

View File

@@ -1,83 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import ColumnManager from './ColumnManager';
import FormViewFilters from '../formview/FormViewFilters';
import ReferenceManager from './ReferenceManager';
import { HorizontalSplitter } from '../widgets/Splitter';
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
import CellDataView from '../celldata/CellDataView';
import useTheme from '../theme/useTheme';
const LeftContainer = styled.div`
background-color: ${props => props.theme.manager_background};
display: flex;
flex: 1;
`;
const DataGridContainer = styled.div`
position: relative;
flex-grow: 1;
`;
export default function DataGrid(props) {
const { GridCore, FormView, formDisplay } = props;
const theme = useTheme();
const [managerSize, setManagerSize] = React.useState(0);
const [selection, setSelection] = React.useState([]);
const [formSelection, setFormSelection] = React.useState(null);
const [grider, setGrider] = React.useState(null);
const [collapsedWidgets, setCollapsedWidgets] = React.useState([]);
// const [formViewData, setFormViewData] = React.useState(null);
const isFormView = !!(formDisplay && formDisplay.config && formDisplay.config.isFormView);
return (
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
<LeftContainer theme={theme}>
<WidgetColumnBar onChangeCollapsedWidgets={setCollapsedWidgets}>
{!isFormView && (
<WidgetColumnBarItem title="Columns" name="columns" height={props.showReferences ? '40%' : '60%'}>
<ColumnManager {...props} managerSize={managerSize} />
</WidgetColumnBarItem>
)}
{isFormView && (
<WidgetColumnBarItem title="Filters" name="filters" height="30%">
<FormViewFilters {...props} managerSize={managerSize} />
</WidgetColumnBarItem>
)}
{props.showReferences && props.display.hasReferences && (
<WidgetColumnBarItem title="References" name="references" height="30%" collapsed={props.isDetailView}>
<ReferenceManager {...props} managerSize={managerSize} />
</WidgetColumnBarItem>
)}
<WidgetColumnBarItem
title="Cell data"
name="cellData"
// cell data must be collapsed by default, because of performance reasons
// when not collapsed, onSelectionChanged of grid is set and RERENDER of this component is done on every selection change
collapsed
>
{isFormView ? (
<CellDataView selectedValue={formSelection} />
) : (
<CellDataView selection={selection} grider={grider} />
)}
</WidgetColumnBarItem>
</WidgetColumnBar>
</LeftContainer>
<DataGridContainer>
{isFormView ? (
<FormView {...props} onSelectionChanged={collapsedWidgets.includes('cellData') ? null : setFormSelection} />
) : (
<GridCore
{...props}
onSelectionChanged={collapsedWidgets.includes('cellData') ? null : setSelection}
onChangeGrider={setGrider}
formViewAvailable={!!FormView && !!formDisplay}
/>
)}
</DataGridContainer>
</HorizontalSplitter>
);
}

View File

@@ -1,68 +0,0 @@
import React from 'react';
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
export default function DataGridContextMenu({
copy,
revertRowChanges,
deleteSelectedRows,
insertNewRow,
setNull,
reload,
exportGrid,
filterSelectedValue,
openQuery,
openFreeTable,
openChartSelection,
openActiveChart,
switchToForm,
}) {
return (
<>
{!!reload && (
<DropDownMenuItem onClick={reload} keyText="F5">
Reload
</DropDownMenuItem>
)}
{!!reload && <DropDownMenuDivider />}
<DropDownMenuItem onClick={copy} keyText="Ctrl+C">
Copy
</DropDownMenuItem>
{revertRowChanges && (
<DropDownMenuItem onClick={revertRowChanges} keyText="Ctrl+R">
Revert row changes
</DropDownMenuItem>
)}
{deleteSelectedRows && (
<DropDownMenuItem onClick={deleteSelectedRows} keyText="Ctrl+Delete">
Delete selected rows
</DropDownMenuItem>
)}
{insertNewRow && (
<DropDownMenuItem onClick={insertNewRow} keyText="Insert">
Insert new row
</DropDownMenuItem>
)}
<DropDownMenuDivider />
{setNull && (
<DropDownMenuItem onClick={setNull} keyText="Ctrl+0">
Set NULL
</DropDownMenuItem>
)}
{exportGrid && <DropDownMenuItem onClick={exportGrid}>Export</DropDownMenuItem>}
{filterSelectedValue && (
<DropDownMenuItem onClick={filterSelectedValue} keyText="Ctrl+F">
Filter selected value
</DropDownMenuItem>
)}
{openQuery && <DropDownMenuItem onClick={openQuery}>Open query</DropDownMenuItem>}
<DropDownMenuItem onClick={openFreeTable}>Open selection in free table editor</DropDownMenuItem>
<DropDownMenuItem onClick={openChartSelection}>Open chart from selection</DropDownMenuItem>
{openActiveChart && <DropDownMenuItem onClick={openActiveChart}>Open active chart</DropDownMenuItem>}
{!!switchToForm && (
<DropDownMenuItem onClick={switchToForm} keyText="F4">
Form view
</DropDownMenuItem>
)}
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,333 +0,0 @@
// @ts-nocheck
import moment from 'moment';
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import InplaceEditor from './InplaceEditor';
import { cellIsSelected } from './gridutil';
import { isTypeLogical } from 'dbgate-tools';
import useTheme from '../theme/useTheme';
import { FontIcon } from '../icons';
const TableBodyCell = styled.td`
font-weight: normal;
border: 1px solid ${props => props.theme.border};
// border-collapse: collapse;
padding: 2px;
white-space: nowrap;
position: relative;
overflow: hidden;
${props =>
props.isSelected &&
!props.isAutofillSelected &&
!props.isFocusedColumn &&
`
background: initial;
background-color: ${props.theme.gridbody_selection[4]};
color: ${props.theme.gridbody_invfont1};`}
${props =>
props.isFrameSelected &&
`
outline: 3px solid ${props.theme.gridbody_selection[4]};
outline-offset: -3px;`}
${props =>
props.isAutofillSelected &&
!props.isFocusedColumn &&
`
outline: 3px solid ${props.theme.gridbody_selection[4]};
outline-offset: -3px;`}
${props =>
props.isModifiedRow &&
!props.isInsertedRow &&
!props.isSelected &&
!props.isAutofillSelected &&
!props.isModifiedCell &&
!props.isFocusedColumn &&
`
background-color: ${props.theme.gridbody_background_gold[1]};`}
${props =>
!props.isSelected &&
!props.isAutofillSelected &&
!props.isInsertedRow &&
!props.isFocusedColumn &&
props.isModifiedCell &&
`
background-color: ${props.theme.gridbody_background_orange[1]};`}
${props =>
!props.isSelected &&
!props.isAutofillSelected &&
!props.isFocusedColumn &&
props.isInsertedRow &&
`
background-color: ${props.theme.gridbody_background_green[1]};`}
${props =>
!props.isSelected &&
!props.isAutofillSelected &&
!props.isFocusedColumn &&
props.isDeletedRow &&
`
background-color: ${props.theme.gridbody_background_volcano[1]};
`}
${props =>
props.isFocusedColumn &&
`
background-color: ${props.theme.gridbody_background_yellow[0]};
`}
${props =>
props.isDeletedRow &&
`
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
// from http://www.patternify.com/
background-repeat: repeat-x;
background-position: 50% 50%;`}
`;
const HintSpan = styled.span`
color: ${props => props.theme.gridbody_font3};
margin-left: 5px;
`;
const NullSpan = styled.span`
color: ${props => props.theme.gridbody_font3};
font-style: italic;
`;
const TableBodyRow = styled.tr`
// height: 35px;
background-color: ${props => props.theme.gridbody_background};
&:nth-child(6n + 3) {
background-color: ${props => props.theme.gridbody_background_alt2};
}
&:nth-child(6n + 6) {
background-color: ${props => props.theme.gridbody_background_alt3};
}
`;
const TableHeaderCell = styled.td`
border: 1px solid ${props => props.theme.border};
text-align: left;
padding: 2px;
background-color: ${props => props.theme.gridheader_background};
overflow: hidden;
position: relative;
`;
const AutoFillPoint = styled.div`
width: 8px;
height: 8px;
background-color: ${props => props.theme.gridbody_selection[6]};
position: absolute;
right: 0px;
bottom: 0px;
overflow: visible;
cursor: crosshair;
`;
export const ShowFormButton = styled.div`
position: absolute;
right: 0px;
top: 1px;
color: ${props => props.theme.gridbody_font3};
background-color: ${props => props.theme.gridheader_background};
border: 1px solid ${props => props.theme.gridheader_background};
&:hover {
color: ${props => props.theme.gridheader_font_hover};
border: 1px solid ${props => props.theme.border};
top: 1px;
right: 0px;
}
`;
function makeBulletString(value) {
return _.pad('', value.length, '•');
}
function highlightSpecialCharacters(value) {
value = value.replace(/\n/g, '↲');
value = value.replace(/\r/g, '');
value = value.replace(/^(\s+)/, makeBulletString);
value = value.replace(/(\s+)$/, makeBulletString);
value = value.replace(/(\s\s+)/g, makeBulletString);
return value;
}
const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
export function CellFormattedValue({ value, dataType, theme }) {
if (value == null) return <NullSpan theme={theme}>(NULL)</NullSpan>;
if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
if (value === true) return '1';
if (value === false) return '0';
if (_.isNumber(value)) {
if (value >= 10000 || value <= -10000) {
return value.toLocaleString();
}
return value.toString();
}
if (_.isString(value)) {
if (dateTimeRegex.test(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
return highlightSpecialCharacters(value);
}
if (_.isPlainObject(value)) {
if (_.isArray(value.data)) {
if (value.data.length == 1 && isTypeLogical(dataType)) return value.data[0];
return <NullSpan theme={theme}>({value.data.length} bytes)</NullSpan>;
}
return <NullSpan theme={theme}>(RAW)</NullSpan>;
}
return value.toString();
}
function RowHeaderCell({ rowIndex, theme, onSetFormView, rowData }) {
const [mouseIn, setMouseIn] = React.useState(false);
return (
<TableHeaderCell
data-row={rowIndex}
data-col="header"
theme={theme}
onMouseEnter={onSetFormView ? () => setMouseIn(true) : null}
onMouseLeave={onSetFormView ? () => setMouseIn(false) : null}
>
{rowIndex + 1}
{!!onSetFormView && mouseIn && (
<ShowFormButton
theme={theme}
onClick={e => {
e.stopPropagation();
onSetFormView(rowData);
}}
>
<FontIcon icon="icon form" />
</ShowFormButton>
)}
</TableHeaderCell>
);
}
/** @param props {import('./types').DataGridProps} */
function DataGridRow(props) {
const {
rowHeight,
rowIndex,
visibleRealColumns,
inplaceEditorState,
dispatchInsplaceEditor,
autofillMarkerCell,
selectedCells,
autofillSelectedCells,
focusedColumn,
grider,
frameSelection,
onSetFormView,
} = props;
// usePropsCompare({
// rowHeight,
// rowIndex,
// visibleRealColumns,
// inplaceEditorState,
// dispatchInsplaceEditor,
// row,
// display,
// changeSet,
// setChangeSet,
// insertedRowIndex,
// autofillMarkerCell,
// selectedCells,
// autofillSelectedCells,
// });
// console.log('RENDER ROW', rowIndex);
const theme = useTheme();
const rowData = grider.getRowData(rowIndex);
const rowStatus = grider.getRowStatus(rowIndex);
const hintFieldsAllowed = visibleRealColumns
.filter(col => {
if (!col.hintColumnName) return false;
if (rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) return false;
return true;
})
.map(col => col.uniqueName);
if (!rowData) return null;
return (
<TableBodyRow style={{ height: `${rowHeight}px` }} theme={theme}>
<RowHeaderCell rowIndex={rowIndex} theme={theme} onSetFormView={onSetFormView} rowData={rowData} />
{visibleRealColumns.map(col => (
<TableBodyCell
key={col.uniqueName}
theme={theme}
style={{
width: col.widthPx,
minWidth: col.widthPx,
maxWidth: col.widthPx,
}}
data-row={rowIndex}
data-col={col.colIndex}
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
isModifiedRow={rowStatus.status == 'updated'}
isFocusedColumn={col.uniqueName == focusedColumn}
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
isInsertedRow={
rowStatus.status == 'inserted' || (rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))
}
isDeletedRow={
rowStatus.status == 'deleted' || (rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))
}
>
{inplaceEditorState.cell &&
rowIndex == inplaceEditorState.cell[0] &&
col.colIndex == inplaceEditorState.cell[1] ? (
<InplaceEditor
widthPx={col.widthPx}
inplaceEditorState={inplaceEditorState}
dispatchInsplaceEditor={dispatchInsplaceEditor}
cellValue={rowData[col.uniqueName]}
// grider={grider}
// rowIndex={rowIndex}
// uniqueName={col.uniqueName}
onSetValue={value => grider.setCellValue(rowIndex, col.uniqueName, value)}
/>
) : (
<>
<CellFormattedValue value={rowData[col.uniqueName]} dataType={col.dataType} theme={theme} />
{hintFieldsAllowed.includes(col.uniqueName) && (
<HintSpan theme={theme}>{rowData[col.hintColumnName]}</HintSpan>
)}
{col.foreignKey && rowData[col.uniqueName] && (
<ShowFormButton
theme={theme}
className="buttonLike"
onClick={e => {
e.stopPropagation();
onSetFormView(rowData, col);
}}
>
<FontIcon icon="icon form" />
</ShowFormButton>
)}
</>
)}
{autofillMarkerCell && autofillMarkerCell[1] == col.colIndex && autofillMarkerCell[0] == rowIndex && (
<AutoFillPoint className="autofillHandleMarker" theme={theme}></AutoFillPoint>
)}
</TableBodyCell>
))}
</TableBodyRow>
);
}
export default React.memo(DataGridRow);

View File

@@ -1,32 +0,0 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
export default function DataGridToolbar({ reload, reconnect, grider, save, switchToForm }) {
return (
<>
{switchToForm && (
<ToolbarButton onClick={switchToForm} icon="icon form">
Form view
</ToolbarButton>
)}
<ToolbarButton onClick={reload} icon="icon reload">
Refresh
</ToolbarButton>
<ToolbarButton onClick={reconnect} icon="icon connection">
Reconnect
</ToolbarButton>
<ToolbarButton disabled={!grider.canUndo} onClick={() => grider.undo()} icon="icon undo">
Undo
</ToolbarButton>
<ToolbarButton disabled={!grider.canRedo} onClick={() => grider.redo()} icon="icon redo">
Redo
</ToolbarButton>
<ToolbarButton disabled={!grider.allowSave} onClick={save} icon="icon save">
Save
</ToolbarButton>
<ToolbarButton disabled={!grider.containsChanges} onClick={() => grider.revertAllChanges()} icon="icon close">
Revert
</ToolbarButton>
</>
);
}

View File

@@ -1,61 +0,0 @@
export interface GriderRowStatus {
status: 'regular' | 'updated' | 'deleted' | 'inserted';
modifiedFields?: Set<string>;
insertedFields?: Set<string>;
deletedFields?: Set<string>;
}
export default abstract class Grider {
abstract getRowData(index): any;
abstract get rowCount(): number;
getRowStatus(index): GriderRowStatus {
const res: GriderRowStatus = {
status: 'regular',
};
return res;
}
beginUpdate() {}
endUpdate() {}
setCellValue(index: number, uniqueName: string, value: any) {}
deleteRow(index: number) {}
insertRow(): number {
return null;
}
revertRowChanges(index: number) {}
revertAllChanges() {}
undo() {}
redo() {}
get editable() {
return false;
}
get canInsert() {
return false;
}
get allowSave() {
return this.containsChanges;
}
get rowCountInUpdate() {
return this.rowCount;
}
get canUndo() {
return false;
}
get canRedo() {
return false;
}
get containsChanges() {
return false;
}
get disableLoadNextPage() {
return false;
}
get errors() {
return null;
}
updateRow(index, changeObject) {
for (const key of Object.keys(changeObject)) {
this.setCellValue(index, key, changeObject[key]);
}
}
}

View File

@@ -1,98 +0,0 @@
// @ts-nocheck
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import keycodes from '../utility/keycodes';
const StyledInput = styled.input`
border: 0px solid;
outline: none;
margin: 0px;
padding: 0px;
`;
export default function InplaceEditor({
widthPx,
// rowIndex,
// uniqueName,
// grider,
cellValue,
inplaceEditorState,
dispatchInsplaceEditor,
onSetValue,
}) {
const editorRef = React.useRef();
const widthRef = React.useRef(widthPx);
const isChangedRef = React.useRef(!!inplaceEditorState.text);
React.useEffect(() => {
const editor = editorRef.current;
editor.value = inplaceEditorState.text || cellValue;
editor.focus();
if (inplaceEditorState.selectAll) {
editor.select();
}
}, []);
function handleBlur() {
if (isChangedRef.current) {
const editor = editorRef.current;
onSetValue(editor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
dispatchInsplaceEditor({ type: 'close' });
}
if (inplaceEditorState.shouldSave) {
const editor = editorRef.current;
if (isChangedRef.current) {
onSetValue(editor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
editor.blur();
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
}
function handleKeyDown(event) {
const editor = editorRef.current;
switch (event.keyCode) {
case keycodes.escape:
isChangedRef.current = false;
dispatchInsplaceEditor({ type: 'close' });
break;
case keycodes.enter:
if (isChangedRef.current) {
// grider.setCellValue(rowIndex, uniqueName, editor.value);
onSetValue(editor.value);
isChangedRef.current = false;
}
editor.blur();
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
break;
case keycodes.s:
if (event.ctrlKey) {
if (isChangedRef.current) {
onSetValue(editor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
event.preventDefault();
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
}
break;
}
}
return (
<StyledInput
onBlur={handleBlur}
ref={editorRef}
type="text"
onChange={() => (isChangedRef.current = true)}
onKeyDown={handleKeyDown}
style={{
width: widthRef.current,
minWidth: widthRef.current,
maxWidth: widthRef.current,
}}
/>
);
}

View File

@@ -1,97 +0,0 @@
import React from 'react';
import axios from '../utility/axios';
import { useSetOpenedTabs } from '../utility/globalState';
import useSocket from '../utility/SocketProvider';
import useShowModal from '../modals/showModal';
import ImportExportModal from '../modals/ImportExportModal';
import LoadingDataGridCore from './LoadingDataGridCore';
import RowsArrayGrider from './RowsArrayGrider';
async function loadDataPage(props, offset, limit) {
const { jslid, display } = props;
const response = await axios.post('jsldata/get-rows', {
jslid,
offset,
limit,
filters: display ? display.compileFilters() : null,
});
return response.data;
}
function dataPageAvailable(props) {
return true;
}
async function loadRowCount(props) {
const { jslid } = props;
const response = await axios.request({
url: 'jsldata/get-stats',
method: 'get',
params: {
jslid,
},
});
return response.data.rowCount;
}
export default function JslDataGridCore(props) {
const { jslid } = props;
const [changeIndex, setChangeIndex] = React.useState(0);
const [rowCountLoaded, setRowCountLoaded] = React.useState(null);
const showModal = useShowModal();
const setOpenedTabs = useSetOpenedTabs();
const socket = useSocket();
function exportGrid() {
const initialValues = {};
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
if (archiveMatch) {
initialValues.sourceStorageType = 'archive';
initialValues.sourceArchiveFolder = archiveMatch[1];
initialValues.sourceList = [archiveMatch[2]];
} else {
initialValues.sourceStorageType = 'jsldata';
initialValues.sourceJslId = jslid;
initialValues.sourceList = ['query-data'];
}
showModal(modalState => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
}
const handleJslDataStats = React.useCallback(
stats => {
if (stats.changeIndex < changeIndex) return;
setChangeIndex(stats.changeIndex);
setRowCountLoaded(stats.rowCount);
},
[changeIndex]
);
React.useEffect(() => {
if (jslid && socket) {
socket.on(`jsldata-stats-${jslid}`, handleJslDataStats);
return () => {
socket.off(`jsldata-stats-${jslid}`, handleJslDataStats);
};
}
}, [jslid]);
return (
<LoadingDataGridCore
{...props}
exportGrid={exportGrid}
loadDataPage={loadDataPage}
dataPageAvailable={dataPageAvailable}
loadRowCount={loadRowCount}
rowCountLoaded={rowCountLoaded}
loadNextDataToken={changeIndex}
onReload={() => setChangeIndex(0)}
griderFactory={RowsArrayGrider.factory}
griderFactoryDeps={RowsArrayGrider.factoryDeps}
/>
);
}

View File

@@ -1,141 +0,0 @@
import React from 'react';
import DataGridCore from './DataGridCore';
export default function LoadingDataGridCore(props) {
const {
display,
loadDataPage,
dataPageAvailable,
loadRowCount,
loadNextDataToken,
onReload,
exportGrid,
openQuery,
griderFactory,
griderFactoryDeps,
onChangeGrider,
rowCountLoaded,
} = props;
const [loadProps, setLoadProps] = React.useState({
isLoading: false,
loadedRows: [],
isLoadedAll: false,
loadedTime: new Date().getTime(),
allRowCount: null,
errorMessage: null,
loadNextDataToken: 0,
});
const { isLoading, loadedRows, isLoadedAll, loadedTime, allRowCount, errorMessage } = loadProps;
const loadedTimeRef = React.useRef(0);
const handleLoadRowCount = async () => {
const rowCount = await loadRowCount(props);
setLoadProps(oldLoadProps => ({
...oldLoadProps,
allRowCount: rowCount,
}));
};
const reload = () => {
setLoadProps({
allRowCount: null,
isLoading: false,
loadedRows: [],
isLoadedAll: false,
loadedTime: new Date().getTime(),
errorMessage: null,
loadNextDataToken: 0,
});
if (onReload) onReload();
};
React.useEffect(() => {
if (props.masterLoadedTime && props.masterLoadedTime > loadedTime) {
display.reload();
}
if (display.cache.refreshTime > loadedTime) {
reload();
}
});
const loadNextData = async () => {
if (isLoading) return;
setLoadProps(oldLoadProps => ({
...oldLoadProps,
isLoading: true,
}));
const loadStart = new Date().getTime();
loadedTimeRef.current = loadStart;
const nextRows = await loadDataPage(props, loadedRows.length, 100);
if (loadedTimeRef.current !== loadStart) {
// new load was dispatched
return;
}
// if (!_.isArray(nextRows)) {
// console.log('Error loading data from server', nextRows);
// nextRows = [];
// }
// console.log('nextRows', nextRows);
if (nextRows.errorMessage) {
setLoadProps(oldLoadProps => ({
...oldLoadProps,
isLoading: false,
errorMessage: nextRows.errorMessage,
}));
} else {
if (allRowCount == null) handleLoadRowCount();
const loadedInfo = {
loadedRows: [...loadedRows, ...nextRows],
loadedTime,
};
setLoadProps(oldLoadProps => ({
...oldLoadProps,
isLoading: false,
isLoadedAll: oldLoadProps.loadNextDataToken == loadNextDataToken && nextRows.length === 0,
loadNextDataToken,
...loadedInfo,
}));
}
};
React.useEffect(() => {
setLoadProps(oldProps => ({
...oldProps,
isLoadedAll: false,
}));
}, [loadNextDataToken]);
const griderProps = { ...props, sourceRows: loadedRows };
const grider = React.useMemo(() => griderFactory(griderProps), griderFactoryDeps(griderProps));
React.useEffect(() => {
if (onChangeGrider) onChangeGrider(grider);
}, [grider]);
const handleLoadNextData = () => {
if (!isLoadedAll && !errorMessage && !grider.disableLoadNextPage) {
if (dataPageAvailable(props)) {
// If not, callbacks to load missing metadata are dispatched
loadNextData();
}
}
};
return (
<DataGridCore
{...props}
loadNextData={handleLoadNextData}
errorMessage={errorMessage}
isLoadedAll={isLoadedAll}
loadedTime={loadedTime}
exportGrid={exportGrid}
allRowCount={rowCountLoaded || allRowCount}
openQuery={openQuery}
isLoading={isLoading}
grider={grider}
/>
);
}

View File

@@ -1,7 +0,0 @@
import styled from 'styled-components';
export const ManagerInnerContainer = styled.div`
flex: 1 1;
overflow-y: auto;
overflow-x: auto;
`;

View File

@@ -1,46 +0,0 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
import styled from 'styled-components';
import dimensions from '../theme/dimensions';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
const Container = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
background: ${props => props.theme.gridheader_background_cyan[0]};
height: ${dimensions.toolBar.height}px;
min-height: ${dimensions.toolBar.height}px;
overflow: hidden;
border-top: 1px solid ${props => props.theme.border};
border-bottom: 1px solid ${props => props.theme.border};
`;
const Header = styled.div`
font-weight: bold;
margin-left: 10px;
display: flex;
`;
const HeaderText = styled.div`
margin-left: 10px;
`;
export default function ReferenceHeader({ reference, onClose }) {
const theme = useTheme();
return (
<Container theme={theme}>
<Header>
<FontIcon icon="img reference" />
<HeaderText>
{reference.pureName} [{reference.columns.map(x => x.refName).join(', ')}] = master [
{reference.columns.map(x => x.baseName).join(', ')}]
</HeaderText>
</Header>
<ToolbarButton icon="icon close" onClick={onClose} patchY={6}>
Close
</ToolbarButton>
</Container>
);
}

View File

@@ -1,114 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { ManagerInnerContainer } from './ManagerStyles';
import SearchInput from '../widgets/SearchInput';
import { filterName } from 'dbgate-datalib';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
const SearchBoxWrapper = styled.div`
display: flex;
margin-bottom: 5px;
`;
const Header = styled.div`
font-weight: bold;
white-space: nowrap;
`;
const LinkContainer = styled.div`
color: ${props => props.theme.manager_font_blue[7]};
margin: 5px;
&:hover {
text-decoration: underline;
}
cursor: pointer;
display: flex;
flex-wrap: nowrap;
`;
const NameContainer = styled.div`
margin-left: 5px;
white-space: nowrap;
`;
function ManagerRow({ tableName, columns, icon, onClick }) {
const theme = useTheme();
return (
<LinkContainer onClick={onClick} theme={theme}>
<FontIcon icon={icon} />
<NameContainer>
{tableName} ({columns.map(x => x.columnName).join(', ')})
</NameContainer>
</LinkContainer>
);
}
/** @param props {import('./types').DataGridProps} */
export default function ReferenceManager(props) {
const [filter, setFilter] = React.useState('');
const { display } = props;
const { baseTable } = display || {};
const { foreignKeys } = baseTable || {};
const { dependencies } = baseTable || {};
return (
<>
<SearchBoxWrapper>
<SearchInput placeholder="Search references" filter={filter} setFilter={setFilter} />
</SearchBoxWrapper>
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
{foreignKeys && foreignKeys.length > 0 && (
<>
<Header>References tables ({foreignKeys.length})</Header>
{foreignKeys
.filter(fk => filterName(filter, fk.refTableName))
.map(fk => (
<ManagerRow
key={fk.constraintName}
icon="img link"
tableName={fk.refTableName}
columns={fk.columns}
onClick={() =>
props.onReferenceClick({
schemaName: fk.refSchemaName,
pureName: fk.refTableName,
columns: fk.columns.map(col => ({
baseName: col.columnName,
refName: col.refColumnName,
})),
})
}
/>
))}
</>
)}
{dependencies && dependencies.length > 0 && (
<>
<Header>Dependend tables ({dependencies.length})</Header>
{dependencies
.filter(fk => filterName(filter, fk.pureName))
.map(fk => (
<ManagerRow
key={fk.constraintName}
icon="img reference"
tableName={fk.pureName}
columns={fk.columns}
onClick={() =>
props.onReferenceClick({
schemaName: fk.schemaName,
pureName: fk.pureName,
columns: fk.columns.map(col => ({
baseName: col.refColumnName,
refName: col.columnName,
})),
})
}
/>
))}
</>
)}
</ManagerInnerContainer>
</>
);
}

View File

@@ -1,20 +0,0 @@
import Grider, { GriderRowStatus } from './Grider';
export default class RowsArrayGrider extends Grider {
constructor(private rows: any[]) {
super();
}
getRowData(index: any) {
return this.rows[index];
}
get rowCount() {
return this.rows.length;
}
static factory({ sourceRows }): RowsArrayGrider {
return new RowsArrayGrider(sourceRows);
}
static factoryDeps({ sourceRows }) {
return [sourceRows];
}
}

View File

@@ -1,222 +0,0 @@
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import useDimensions from '../utility/useDimensions';
const StyledHorizontalScrollBar = styled.div`
overflow-x: scroll;
height: 16px;
position: absolute;
bottom: 0;
//left: 100px;
// right: 20px;
right: 0;
left: 0;
`;
const StyledHorizontalScrollContent = styled.div``;
const StyledVerticalScrollBar = styled.div`
overflow-y: scroll;
width: 20px;
position: absolute;
right: 0px;
width: 20px;
bottom: 16px;
// bottom: 0;
top: 0;
`;
const StyledVerticalScrollContent = styled.div``;
export function HorizontalScrollBar({
onScroll = undefined,
valueToSet = undefined,
valueToSetDate = undefined,
minimum,
maximum,
viewportRatio = 0.5,
}) {
const [ref, { width }, node] = useDimensions();
const contentSize = Math.round(width / viewportRatio);
React.useEffect(() => {
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
const position = position01 * (contentSize - width);
if (node) node.scrollLeft = Math.floor(position);
}, [valueToSetDate]);
const handleScroll = () => {
const position = node.scrollLeft;
const ratio = position / (contentSize - width);
if (ratio < 0) return 0;
let res = ratio * (maximum - minimum + 1) + minimum;
onScroll(Math.floor(res + 0.3));
};
return (
<StyledHorizontalScrollBar ref={ref} onScroll={handleScroll}>
<StyledHorizontalScrollContent style={{ width: `${contentSize}px` }}>&nbsp;</StyledHorizontalScrollContent>
</StyledHorizontalScrollBar>
);
}
export function VerticalScrollBar({
onScroll,
valueToSet = undefined,
valueToSetDate = undefined,
minimum,
maximum,
viewportRatio = 0.5,
}) {
const [ref, { height }, node] = useDimensions();
const contentSize = Math.round(height / viewportRatio);
React.useEffect(() => {
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
const position = position01 * (contentSize - height);
if (node) node.scrollTop = Math.floor(position);
}, [valueToSetDate]);
const handleScroll = () => {
const position = node.scrollTop;
const ratio = position / (contentSize - height);
if (ratio < 0) return 0;
let res = ratio * (maximum - minimum + 1) + minimum;
onScroll(Math.floor(res + 0.3));
};
return (
<StyledVerticalScrollBar ref={ref} onScroll={handleScroll}>
<StyledVerticalScrollContent style={{ height: `${contentSize}px` }}>&nbsp;</StyledVerticalScrollContent>
</StyledVerticalScrollBar>
);
}
// export interface IScrollBarProps {
// viewportRatio: number;
// minimum: number;
// maximum: number;
// containerStyle: any;
// onScroll?: any;
// }
// export abstract class ScrollBarBase extends React.Component<IScrollBarProps, {}> {
// domScrollContainer: Element;
// domScrollContent: Element;
// contentSize: number;
// containerResizedBind: any;
// constructor(props) {
// super(props);
// this.containerResizedBind = this.containerResized.bind(this);
// }
// componentDidMount() {
// $(this.domScrollContainer).scroll(this.onScroll.bind(this));
// createResizeDetector(this.domScrollContainer, this.containerResized.bind(this));
// window.addEventListener('resize', this.containerResizedBind);
// this.updateContentSize();
// }
// componentWillUnmount() {
// deleteResizeDetector(this.domScrollContainer);
// window.removeEventListener('resize', this.containerResizedBind);
// }
// onScroll() {
// if (this.props.onScroll) {
// this.props.onScroll(this.value);
// }
// }
// get value(): number {
// let position = this.getScrollPosition();
// let ratio = position / (this.contentSize - this.getContainerSize());
// if (ratio < 0) return 0;
// let res = ratio * (this.props.maximum - this.props.minimum + 1) + this.props.minimum;
// return Math.floor(res + 0.3);
// }
// set value(value: number) {
// let position01 = (value - this.props.minimum) / (this.props.maximum - this.props.minimum + 1);
// let position = position01 * (this.contentSize - this.getContainerSize());
// this.setScrollPosition(Math.floor(position));
// }
// containerResized() {
// this.setContentSizeField();
// this.updateContentSize();
// }
// setContentSizeField() {
// let lastContentSize = this.contentSize;
// this.contentSize = Math.round(this.getContainerSize() / this.props.viewportRatio);
// if (_.isNaN(this.contentSize)) this.contentSize = 0;
// if (this.contentSize > 1000000 && detectBrowser() == BrowserType.Firefox) this.contentSize = 1000000;
// if (lastContentSize != this.contentSize) {
// this.updateContentSize();
// }
// }
// abstract getContainerSize(): number;
// abstract updateContentSize();
// abstract getScrollPosition(): number;
// abstract setScrollPosition(value: number);
// }
// export class HorizontalScrollBar extends ScrollBarBase {
// render() {
// this.setContentSizeField();
// return <div className='ReactGridHorizontalScrollBar' ref={x => this.domScrollContainer = x} style={this.props.containerStyle}>
// <div className='ReactGridHorizontalScrollContent' ref={x => this.domScrollContent = x} style={{ width: this.contentSize }}>
// &nbsp;
// </div>
// </div>;
// }
// getContainerSize(): number {
// return $(this.domScrollContainer).width();
// }
// updateContentSize() {
// $(this.domScrollContent).width(this.contentSize);
// }
// getScrollPosition() {
// return $(this.domScrollContainer).scrollLeft();
// }
// setScrollPosition(value: number) {
// $(this.domScrollContainer).scrollLeft(value);
// }
// }
// export class VerticalScrollBar extends ScrollBarBase {
// render() {
// this.setContentSizeField();
// return <div className='ReactGridVerticalScrollBar' ref={x => this.domScrollContainer = x} style={this.props.containerStyle}>
// <div className='ReactGridVerticalScrollContent' ref={x => this.domScrollContent = x} style={{ height: this.contentSize }}>
// &nbsp;
// </div>
// </div>;
// }
// getContainerSize(): number {
// return $(this.domScrollContainer).height();
// }
// updateContentSize() {
// $(this.domScrollContent).height(this.contentSize);
// }
// getScrollPosition() {
// return $(this.domScrollContainer).scrollTop();
// }
// setScrollPosition(value: number) {
// $(this.domScrollContainer).scrollTop(value);
// }
// }

View File

@@ -1,340 +0,0 @@
import _ from 'lodash';
export class SeriesSizeItem {
constructor() {
this.scrollIndex = -1;
this.frozenIndex = -1;
this.modelIndex = 0;
this.size = 0;
this.position = 0;
}
// modelIndex;
// size;
// position;
get endPosition() {
return this.position + this.size;
}
}
export class SeriesSizes {
constructor() {
this.scrollItems = [];
this.sizeOverridesByModelIndex = {};
this.positions = [];
this.scrollIndexes = [];
this.frozenItems = [];
this.hiddenAndFrozenModelIndexes = null;
this.frozenModelIndexes = null;
this.count = 0;
this.maxSize = 1000;
this.defaultSize = 50;
}
// private sizeOverridesByModelIndex: { [id] } = {};
// count;
// defaultSize;
// maxSize;
// private hiddenAndFrozenModelIndexes[] = [];
// private frozenModelIndexes[] = [];
// private hiddenModelIndexes[] = [];
// private scrollItems: SeriesSizeItem[] = [];
// private positions[] = [];
// private scrollIndexes[] = [];
// private frozenItems: SeriesSizeItem[] = [];
get scrollCount() {
return this.count - (this.hiddenAndFrozenModelIndexes != null ? this.hiddenAndFrozenModelIndexes.length : 0);
}
get frozenCount() {
return this.frozenModelIndexes != null ? this.frozenModelIndexes.length : 0;
}
get frozenSize() {
return _.sumBy(this.frozenItems, x => x.size);
}
get realCount() {
return this.frozenCount + this.scrollCount;
}
putSizeOverride(modelIndex, size, sizeByUser = false) {
if (this.maxSize && size > this.maxSize && !sizeByUser) {
size = this.maxSize;
}
let currentSize = this.sizeOverridesByModelIndex[modelIndex];
if (sizeByUser || !currentSize || size > currentSize) {
this.sizeOverridesByModelIndex[modelIndex] = size;
}
// if (!_.has(this.sizeOverridesByModelIndex, modelIndex))
// this.sizeOverridesByModelIndex[modelIndex] = size;
// if (size > this.sizeOverridesByModelIndex[modelIndex])
// this.sizeOverridesByModelIndex[modelIndex] = size;
}
buildIndex() {
this.scrollItems = [];
this.scrollIndexes = _.filter(
_.map(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelToReal(x) - this.frozenCount),
x => x >= 0
);
this.scrollIndexes.sort();
let lastScrollIndex = -1;
let lastEndPosition = 0;
this.scrollIndexes.forEach(scrollIndex => {
let modelIndex = this.realToModel(scrollIndex + this.frozenCount);
let size = this.sizeOverridesByModelIndex[modelIndex];
let item = new SeriesSizeItem();
item.scrollIndex = scrollIndex;
item.modelIndex = modelIndex;
item.size = size;
item.position = lastEndPosition + (scrollIndex - lastScrollIndex - 1) * this.defaultSize;
this.scrollItems.push(item);
lastScrollIndex = scrollIndex;
lastEndPosition = item.endPosition;
});
this.positions = _.map(this.scrollItems, x => x.position);
this.frozenItems = [];
let lastpos = 0;
for (let i = 0; i < this.frozenCount; i++) {
let modelIndex = this.frozenModelIndexes[i];
let size = this.getSizeByModelIndex(modelIndex);
let item = new SeriesSizeItem();
item.frozenIndex = i;
item.modelIndex = modelIndex;
item.size = size;
item.position = lastpos;
this.frozenItems.push(item);
lastpos += size;
}
}
getScrollIndexOnPosition(position) {
let itemOrder = _.sortedIndex(this.positions, position);
if (this.positions[itemOrder] == position) return itemOrder;
if (itemOrder == 0) return Math.floor(position / this.defaultSize);
if (position <= this.scrollItems[itemOrder - 1].endPosition) return this.scrollItems[itemOrder - 1].scrollIndex;
return (
Math.floor((position - this.scrollItems[itemOrder - 1].position) / this.defaultSize) +
this.scrollItems[itemOrder - 1].scrollIndex
);
}
getFrozenIndexOnPosition(position) {
this.frozenItems.forEach(function (item) {
if (position >= item.position && position <= item.endPosition) return item.frozenIndex;
});
return -1;
}
// getSizeSum(startScrollIndex, endScrollIndex) {
// let order1 = _.sortedIndexOf(this.scrollIndexes, startScrollIndex);
// let order2 = _.sortedIndexOf(this.scrollIndexes, endScrollIndex);
// let count = endScrollIndex - startScrollIndex;
// if (order1 < 0)
// order1 = ~order1;
// if (order2 < 0)
// order2 = ~order2;
// let result = 0;
// for (let i = order1; i <= order2; i++) {
// if (i < 0)
// continue;
// if (i >= this.scrollItems.length)
// continue;
// let item = this.scrollItems[i];
// if (item.scrollIndex < startScrollIndex)
// continue;
// if (item.scrollIndex >= endScrollIndex)
// continue;
// result += item.size;
// count--;
// }
// result += count * this.defaultSize;
// return result;
// }
getSizeByModelIndex(modelIndex) {
if (_.has(this.sizeOverridesByModelIndex, modelIndex)) return this.sizeOverridesByModelIndex[modelIndex];
return this.defaultSize;
}
getSizeByScrollIndex(scrollIndex) {
return this.getSizeByRealIndex(scrollIndex + this.frozenCount);
}
getSizeByRealIndex(realIndex) {
let modelIndex = this.realToModel(realIndex);
return this.getSizeByModelIndex(modelIndex);
}
removeSizeOverride(realIndex) {
let modelIndex = this.realToModel(realIndex);
delete this.sizeOverridesByModelIndex[modelIndex];
}
getScroll(sourceScrollIndex, targetScrollIndex) {
if (sourceScrollIndex < targetScrollIndex) {
return -_.sum(
_.map(_.range(sourceScrollIndex, targetScrollIndex - sourceScrollIndex), x => this.getSizeByScrollIndex(x))
);
} else {
return _.sum(
_.map(_.range(targetScrollIndex, sourceScrollIndex - targetScrollIndex), x => this.getSizeByScrollIndex(x))
);
}
}
modelIndexIsInScrollArea(modelIndex) {
let realIndex = this.modelToReal(modelIndex);
return realIndex >= this.frozenCount;
}
getTotalScrollSizeSum() {
let scrollSizeOverrides = _.map(
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelIndexIsInScrollArea(x)),
x => this.sizeOverridesByModelIndex[x]
);
return _.sum(scrollSizeOverrides) + (this.count - scrollSizeOverrides.length) * this.defaultSize;
}
getVisibleScrollSizeSum() {
let scrollSizeOverrides = _.map(
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => !_.includes(this.hiddenAndFrozenModelIndexes, x)),
x => this.sizeOverridesByModelIndex[x]
);
return (
_.sum(scrollSizeOverrides) +
(this.count - this.hiddenModelIndexes.length - scrollSizeOverrides.length) * this.defaultSize
);
}
intKeys(value) {
return _.keys(value).map(x => _.parseInt(x));
}
getPositionByRealIndex(realIndex) {
if (realIndex < 0) return 0;
if (realIndex < this.frozenCount) return this.frozenItems[realIndex].position;
return this.getPositionByScrollIndex(realIndex - this.frozenCount);
}
getPositionByScrollIndex(scrollIndex) {
let order = _.sortedIndex(this.scrollIndexes, scrollIndex);
if (this.scrollIndexes[order] == scrollIndex) return this.scrollItems[order].position;
order--;
if (order < 0) return scrollIndex * this.defaultSize;
return (
this.scrollItems[order].endPosition + (scrollIndex - this.scrollItems[order].scrollIndex - 1) * this.defaultSize
);
}
getVisibleScrollCount(firstVisibleIndex, viewportSize) {
let res = 0;
let index = firstVisibleIndex;
let count = 0;
while (res < viewportSize && index <= this.scrollCount) {
// console.log('this.getSizeByScrollIndex(index)', this.getSizeByScrollIndex(index));
res += this.getSizeByScrollIndex(index);
index++;
count++;
}
// console.log('getVisibleScrollCount', firstVisibleIndex, viewportSize, count);
return count;
}
getVisibleScrollCountReversed(lastVisibleIndex, viewportSize) {
let res = 0;
let index = lastVisibleIndex;
let count = 0;
while (res < viewportSize && index >= 0) {
res += this.getSizeByScrollIndex(index);
index--;
count++;
}
return count;
}
invalidateAfterScroll(oldFirstVisible, newFirstVisible, invalidate, viewportSize) {
if (newFirstVisible > oldFirstVisible) {
let oldVisibleCount = this.getVisibleScrollCount(oldFirstVisible, viewportSize);
let newVisibleCount = this.getVisibleScrollCount(newFirstVisible, viewportSize);
for (let i = oldFirstVisible + oldVisibleCount - 1; i <= newFirstVisible + newVisibleCount; i++) {
invalidate(i + this.frozenCount);
}
} else {
for (let i = newFirstVisible; i <= oldFirstVisible; i++) {
invalidate(i + this.frozenCount);
}
}
}
isWholeInView(firstVisibleIndex, index, viewportSize) {
let res = 0;
let testedIndex = firstVisibleIndex;
while (res < viewportSize && testedIndex < this.count) {
res += this.getSizeByScrollIndex(testedIndex);
if (testedIndex == index) return res <= viewportSize;
testedIndex++;
}
return false;
}
scrollInView(firstVisibleIndex, scrollIndex, viewportSize) {
if (this.isWholeInView(firstVisibleIndex, scrollIndex, viewportSize)) {
return firstVisibleIndex;
}
if (scrollIndex < firstVisibleIndex) {
return scrollIndex;
}
let res = 0;
let testedIndex = scrollIndex;
while (res < viewportSize && testedIndex >= 0) {
let size = this.getSizeByScrollIndex(testedIndex);
if (res + size > viewportSize) return testedIndex + 1;
testedIndex--;
res += size;
}
if (res >= viewportSize && testedIndex < scrollIndex) return testedIndex + 1;
return firstVisibleIndex;
}
resize(realIndex, newSize) {
if (realIndex < 0) return;
let modelIndex = this.realToModel(realIndex);
if (modelIndex < 0) return;
this.sizeOverridesByModelIndex[modelIndex] = newSize;
this.buildIndex();
}
setExtraordinaryIndexes(hidden, frozen) {
//this._hiddenAndFrozenModelIndexes = _.clone(hidden);
hidden = hidden.filter(x => x >= 0);
frozen = frozen.filter(x => x >= 0);
hidden.sort((a, b) => a - b);
frozen.sort((a, b) => a - b);
this.frozenModelIndexes = _.filter(frozen, x => !_.includes(hidden, x));
this.hiddenModelIndexes = _.filter(hidden, x => !_.includes(frozen, x));
this.hiddenAndFrozenModelIndexes = _.concat(hidden, this.frozenModelIndexes);
this.frozenModelIndexes.sort((a, b) => a - b);
if (this.hiddenAndFrozenModelIndexes.length == 0) this.hiddenAndFrozenModelIndexes = null;
if (this.frozenModelIndexes.length == 0) this.frozenModelIndexes = null;
this.buildIndex();
}
realToModel(realIndex) {
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return realIndex;
if (realIndex < 0) return -1;
if (realIndex < this.frozenCount && this.frozenModelIndexes != null) return this.frozenModelIndexes[realIndex];
if (this.hiddenAndFrozenModelIndexes == null) return realIndex;
realIndex -= this.frozenCount;
for (let hidItem of this.hiddenAndFrozenModelIndexes) {
if (realIndex < hidItem) return realIndex;
realIndex++;
}
return realIndex;
}
modelToReal(modelIndex) {
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return modelIndex;
if (modelIndex < 0) return -1;
let frozenIndex = this.frozenModelIndexes != null ? _.indexOf(this.frozenModelIndexes, modelIndex) : -1;
if (frozenIndex >= 0) return frozenIndex;
if (this.hiddenAndFrozenModelIndexes == null) return modelIndex;
let hiddenIndex = _.sortedIndex(this.hiddenAndFrozenModelIndexes, modelIndex);
if (this.hiddenAndFrozenModelIndexes[hiddenIndex] == modelIndex) return -1;
if (hiddenIndex >= 0) return modelIndex - hiddenIndex + this.frozenCount;
return modelIndex;
}
getFrozenPosition(frozenIndex) {
return this.frozenItems[frozenIndex].position;
}
hasSizeOverride(modelIndex) {
return _.has(this.sizeOverridesByModelIndex, modelIndex);
}
isVisible(testedRealIndex, firstVisibleScrollIndex, viewportSize) {
if (testedRealIndex < 0) return false;
if (testedRealIndex >= 0 && testedRealIndex < this.frozenCount) return true;
let scrollIndex = testedRealIndex - this.frozenCount;
let onPageIndex = scrollIndex - firstVisibleScrollIndex;
return onPageIndex >= 0 && onPageIndex < this.getVisibleScrollCount(firstVisibleScrollIndex, viewportSize);
}
}

View File

@@ -1,341 +0,0 @@
import _ from 'lodash';
export class SeriesSizeItem {
public scrollIndex: number = -1;
public modelIndex: number;
public frozenIndex: number = -1;
public size: number;
public position: number;
public get endPosition(): number {
return this.position + this.size;
}
}
export class SeriesSizes {
private sizeOverridesByModelIndex: { [id: number]: number } = {};
public count: number = 0;
public defaultSize: number = 50;
public maxSize: number = 1000;
private hiddenAndFrozenModelIndexes: number[] = [];
private frozenModelIndexes: number[] = [];
private hiddenModelIndexes: number[] = [];
private scrollItems: SeriesSizeItem[] = [];
private positions: number[] = [];
private scrollIndexes: number[] = [];
private frozenItems: SeriesSizeItem[] = [];
public get scrollCount(): number {
return this.count - (this.hiddenAndFrozenModelIndexes != null ? this.hiddenAndFrozenModelIndexes.length : 0);
}
public get frozenCount(): number {
return this.frozenModelIndexes != null ? this.frozenModelIndexes.length : 0;
}
public get frozenSize(): number {
return _.sumBy(this.frozenItems, x => x.size);
}
public get realCount(): number {
return this.frozenCount + this.scrollCount;
}
// public clear(): void {
// this.scrollItems = [];
// this.sizeOverridesByModelIndex = {};
// this.positions = [];
// this.scrollIndexes = [];
// this.frozenItems = [];
// this.hiddenAndFrozenModelIndexes = null;
// this.frozenModelIndexes = null;
// }
public putSizeOverride(modelIndex: number, size: number, sizeByUser = false): void {
if (this.maxSize && size > this.maxSize && !sizeByUser) {
size = this.maxSize;
}
let currentSize = this.sizeOverridesByModelIndex[modelIndex];
if (sizeByUser || !currentSize || size > currentSize) {
this.sizeOverridesByModelIndex[modelIndex] = size;
}
// if (!_.has(this.sizeOverridesByModelIndex, modelIndex))
// this.sizeOverridesByModelIndex[modelIndex] = size;
// if (size > this.sizeOverridesByModelIndex[modelIndex])
// this.sizeOverridesByModelIndex[modelIndex] = size;
}
public buildIndex(): void {
this.scrollItems = [];
this.scrollIndexes = _.filter(
_.map(_.range(this.count), x => this.modelToReal(x) - this.frozenCount),
// _.map(this.intKeys(_.keys(this.sizeOverridesByModelIndex)), (x) => this.modelToReal(x) - this.frozenCount),
x => x >= 0
);
this.scrollIndexes.sort();
let lastScrollIndex: number = -1;
let lastEndPosition: number = 0;
this.scrollIndexes.forEach(scrollIndex => {
let modelIndex: number = this.realToModel(scrollIndex + this.frozenCount);
let size: number = this.sizeOverridesByModelIndex[modelIndex];
let item = new SeriesSizeItem();
item.scrollIndex = scrollIndex;
item.modelIndex = modelIndex;
item.size = size;
item.position = lastEndPosition + (scrollIndex - lastScrollIndex - 1) * this.defaultSize;
this.scrollItems.push(item);
lastScrollIndex = scrollIndex;
lastEndPosition = item.endPosition;
});
this.positions = _.map(this.scrollItems, x => x.position);
this.frozenItems = [];
let lastpos: number = 0;
for (let i: number = 0; i < this.frozenCount; i++) {
let modelIndex: number = this.frozenModelIndexes[i];
let size: number = this.getSizeByModelIndex(modelIndex);
let item = new SeriesSizeItem();
item.frozenIndex = i;
item.modelIndex = modelIndex;
item.size = size;
item.position = lastpos;
this.frozenItems.push(item);
lastpos += size;
}
}
public getScrollIndexOnPosition(position: number): number {
let itemOrder: number = _.sortedIndex(this.positions, position);
if (this.positions[itemOrder] == position) return itemOrder;
if (itemOrder == 0) return Math.floor(position / this.defaultSize);
if (position <= this.scrollItems[itemOrder - 1].endPosition) return this.scrollItems[itemOrder - 1].scrollIndex;
return (
Math.floor((position - this.scrollItems[itemOrder - 1].position) / this.defaultSize) +
this.scrollItems[itemOrder - 1].scrollIndex
);
}
public getFrozenIndexOnPosition(position: number): number {
this.frozenItems.forEach(function (item) {
if (position >= item.position && position <= item.endPosition) return item.frozenIndex;
});
return -1;
}
// public getSizeSum(startScrollIndex: number, endScrollIndex: number): number {
// let order1: number = _.sortedIndexOf(this.scrollIndexes, startScrollIndex);
// let order2: number = _.sortedIndexOf(this.scrollIndexes, endScrollIndex);
// let count: number = endScrollIndex - startScrollIndex;
// if (order1 < 0)
// order1 = ~order1;
// if (order2 < 0)
// order2 = ~order2;
// let result: number = 0;
// for (let i: number = order1; i <= order2; i++) {
// if (i < 0)
// continue;
// if (i >= this.scrollItems.length)
// continue;
// let item = this.scrollItems[i];
// if (item.scrollIndex < startScrollIndex)
// continue;
// if (item.scrollIndex >= endScrollIndex)
// continue;
// result += item.size;
// count--;
// }
// result += count * this.defaultSize;
// return result;
// }
public getSizeByModelIndex(modelIndex: number): number {
if (_.has(this.sizeOverridesByModelIndex, modelIndex)) return this.sizeOverridesByModelIndex[modelIndex];
return this.defaultSize;
}
public getSizeByScrollIndex(scrollIndex: number): number {
return this.getSizeByRealIndex(scrollIndex + this.frozenCount);
}
public getSizeByRealIndex(realIndex: number): number {
let modelIndex: number = this.realToModel(realIndex);
return this.getSizeByModelIndex(modelIndex);
}
public removeSizeOverride(realIndex: number): void {
let modelIndex: number = this.realToModel(realIndex);
delete this.sizeOverridesByModelIndex[modelIndex];
}
public getScroll(sourceScrollIndex: number, targetScrollIndex: number): number {
if (sourceScrollIndex < targetScrollIndex) {
return -_.sum(
_.map(_.range(sourceScrollIndex, targetScrollIndex - sourceScrollIndex), x => this.getSizeByScrollIndex(x))
);
} else {
return _.sum(
_.map(_.range(targetScrollIndex, sourceScrollIndex - targetScrollIndex), x => this.getSizeByScrollIndex(x))
);
}
}
public modelIndexIsInScrollArea(modelIndex: number): boolean {
let realIndex = this.modelToReal(modelIndex);
return realIndex >= this.frozenCount;
}
public getTotalScrollSizeSum(): number {
let scrollSizeOverrides = _.map(
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelIndexIsInScrollArea(x)),
x => this.sizeOverridesByModelIndex[x]
);
return _.sum(scrollSizeOverrides) + (this.count - scrollSizeOverrides.length) * this.defaultSize;
}
public getVisibleScrollSizeSum(): number {
let scrollSizeOverrides = _.map(
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => !_.includes(this.hiddenAndFrozenModelIndexes, x)),
x => this.sizeOverridesByModelIndex[x]
);
return (
_.sum(scrollSizeOverrides) +
(this.count - this.hiddenModelIndexes.length - scrollSizeOverrides.length) * this.defaultSize
);
}
private intKeys(value): number[] {
return _.keys(value).map(x => _.parseInt(x));
}
public getPositionByRealIndex(realIndex: number): number {
if (realIndex < 0) return 0;
if (realIndex < this.frozenCount) return this.frozenItems[realIndex].position;
return this.getPositionByScrollIndex(realIndex - this.frozenCount);
}
public getPositionByScrollIndex(scrollIndex: number): number {
let order: number = _.sortedIndex(this.scrollIndexes, scrollIndex);
if (this.scrollIndexes[order] == scrollIndex) return this.scrollItems[order].position;
order--;
if (order < 0) return scrollIndex * this.defaultSize;
return (
this.scrollItems[order].endPosition + (scrollIndex - this.scrollItems[order].scrollIndex - 1) * this.defaultSize
);
}
public getVisibleScrollCount(firstVisibleIndex: number, viewportSize: number): number {
let res: number = 0;
let index: number = firstVisibleIndex;
let count: number = 0;
while (res < viewportSize && index <= this.scrollCount) {
res += this.getSizeByScrollIndex(index);
index++;
count++;
}
return count;
}
public getVisibleScrollCountReversed(lastVisibleIndex: number, viewportSize: number): number {
let res: number = 0;
let index: number = lastVisibleIndex;
let count: number = 0;
while (res < viewportSize && index >= 0) {
res += this.getSizeByScrollIndex(index);
index--;
count++;
}
return count;
}
public invalidateAfterScroll(
oldFirstVisible: number,
newFirstVisible: number,
invalidate: (_: number) => void,
viewportSize: number
): void {
if (newFirstVisible > oldFirstVisible) {
let oldVisibleCount: number = this.getVisibleScrollCount(oldFirstVisible, viewportSize);
let newVisibleCount: number = this.getVisibleScrollCount(newFirstVisible, viewportSize);
for (let i: number = oldFirstVisible + oldVisibleCount - 1; i <= newFirstVisible + newVisibleCount; i++) {
invalidate(i + this.frozenCount);
}
} else {
for (let i: number = newFirstVisible; i <= oldFirstVisible; i++) {
invalidate(i + this.frozenCount);
}
}
}
public isWholeInView(firstVisibleIndex: number, index: number, viewportSize: number): boolean {
let res: number = 0;
let testedIndex: number = firstVisibleIndex;
while (res < viewportSize && testedIndex < this.count) {
res += this.getSizeByScrollIndex(testedIndex);
if (testedIndex == index) return res <= viewportSize;
testedIndex++;
}
return false;
}
public scrollInView(firstVisibleIndex: number, scrollIndex: number, viewportSize: number): number {
if (this.isWholeInView(firstVisibleIndex, scrollIndex, viewportSize)) {
return firstVisibleIndex;
}
if (scrollIndex < firstVisibleIndex) {
return scrollIndex;
}
let testedIndex = firstVisibleIndex + 1;
while (testedIndex < this.scrollCount) {
if (this.isWholeInView(testedIndex, scrollIndex, viewportSize)) {
return testedIndex;
}
testedIndex++;
}
return this.scrollCount - 1;
// let res: number = 0;
// let testedIndex: number = scrollIndex;
// while (res < viewportSize && testedIndex >= 0) {
// let size: number = this.getSizeByScrollIndex(testedIndex);
// if (res + size > viewportSize) return testedIndex + 1;
// testedIndex--;
// res += size;
// }
// if (res >= viewportSize && testedIndex < scrollIndex) return testedIndex + 1;
// return firstVisibleIndex;
}
public resize(realIndex: number, newSize: number): void {
if (realIndex < 0) return;
let modelIndex: number = this.realToModel(realIndex);
if (modelIndex < 0) return;
this.sizeOverridesByModelIndex[modelIndex] = newSize;
this.buildIndex();
}
public setExtraordinaryIndexes(hidden: number[], frozen: number[]): void {
//this._hiddenAndFrozenModelIndexes = _.clone(hidden);
hidden = hidden.filter(x => x >= 0);
frozen = frozen.filter(x => x >= 0);
hidden.sort((a, b) => a - b);
frozen.sort((a, b) => a - b);
this.frozenModelIndexes = _.filter(frozen, x => !_.includes(hidden, x));
this.hiddenModelIndexes = _.filter(hidden, x => !_.includes(frozen, x));
this.hiddenAndFrozenModelIndexes = _.concat(hidden, this.frozenModelIndexes);
this.frozenModelIndexes.sort((a, b) => a - b);
if (this.hiddenAndFrozenModelIndexes.length == 0) this.hiddenAndFrozenModelIndexes = null;
if (this.frozenModelIndexes.length == 0) this.frozenModelIndexes = null;
this.buildIndex();
}
public realToModel(realIndex: number): number {
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return realIndex;
if (realIndex < 0) return -1;
if (realIndex < this.frozenCount && this.frozenModelIndexes != null) return this.frozenModelIndexes[realIndex];
if (this.hiddenAndFrozenModelIndexes == null) return realIndex;
realIndex -= this.frozenCount;
for (let hidItem of this.hiddenAndFrozenModelIndexes) {
if (realIndex < hidItem) return realIndex;
realIndex++;
}
return realIndex;
}
public modelToReal(modelIndex: number): number {
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return modelIndex;
if (modelIndex < 0) return -1;
let frozenIndex: number = this.frozenModelIndexes != null ? _.indexOf(this.frozenModelIndexes, modelIndex) : -1;
if (frozenIndex >= 0) return frozenIndex;
if (this.hiddenAndFrozenModelIndexes == null) return modelIndex;
let hiddenIndex: number = _.sortedIndex(this.hiddenAndFrozenModelIndexes, modelIndex);
if (this.hiddenAndFrozenModelIndexes[hiddenIndex] == modelIndex) return -1;
if (hiddenIndex >= 0) return modelIndex - hiddenIndex + this.frozenCount;
return modelIndex;
}
public getFrozenPosition(frozenIndex: number): number {
return this.frozenItems[frozenIndex].position;
}
public hasSizeOverride(modelIndex: number): boolean {
return _.has(this.sizeOverridesByModelIndex, modelIndex);
}
public isVisible(testedRealIndex: number, firstVisibleScrollIndex: number, viewportSize: number): boolean {
if (testedRealIndex < 0) return false;
if (testedRealIndex >= 0 && testedRealIndex < this.frozenCount) return true;
let scrollIndex: number = testedRealIndex - this.frozenCount;
let onPageIndex: number = scrollIndex - firstVisibleScrollIndex;
return onPageIndex >= 0 && onPageIndex < this.getVisibleScrollCount(firstVisibleScrollIndex, viewportSize);
}
}

View File

@@ -1,177 +0,0 @@
import React from 'react';
import axios from '../utility/axios';
import { useSetOpenedTabs } from '../utility/globalState';
import DataGridCore from './DataGridCore';
import useSocket from '../utility/SocketProvider';
import useShowModal from '../modals/showModal';
import ImportExportModal from '../modals/ImportExportModal';
import { changeSetToSql, createChangeSet, getChangeSetInsertedRows } from 'dbgate-datalib';
import LoadingDataGridCore from './LoadingDataGridCore';
import ChangeSetGrider from './ChangeSetGrider';
import { scriptToSql } from 'dbgate-sqltree';
import useModalState from '../modals/useModalState';
import ConfirmSqlModal from '../modals/ConfirmSqlModal';
import ErrorMessageModal from '../modals/ErrorMessageModal';
import useOpenNewTab from '../utility/useOpenNewTab';
/** @param props {import('./types').DataGridProps} */
async function loadDataPage(props, offset, limit) {
const { display, conid, database } = props;
const sql = display.getPageQuery(offset, limit);
const response = await axios.request({
url: 'database-connections/query-data',
method: 'post',
params: {
conid,
database,
},
data: { sql },
});
if (response.data.errorMessage) return response.data;
return response.data.rows;
}
function dataPageAvailable(props) {
const { display } = props;
const sql = display.getPageQuery(0, 1);
return !!sql;
}
async function loadRowCount(props) {
const { display, conid, database } = props;
const sql = display.getCountQuery();
const response = await axios.request({
url: 'database-connections/query-data',
method: 'post',
params: {
conid,
database,
},
data: { sql },
});
return parseInt(response.data.rows[0].count);
}
/** @param props {import('./types').DataGridProps} */
export default function SqlDataGridCore(props) {
const { conid, database, display, changeSetState, dispatchChangeSet } = props;
const showModal = useShowModal();
const openNewTab = useOpenNewTab();
const confirmSqlModalState = useModalState();
const [confirmSql, setConfirmSql] = React.useState('');
const changeSet = changeSetState && changeSetState.value;
const changeSetRef = React.useRef(changeSet);
changeSetRef.current = changeSet;
function exportGrid() {
const initialValues = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceSql = display.getExportQuery();
initialValues.sourceList = display.baseTable ? [display.baseTable.pureName] : [];
showModal(modalState => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
}
function openActiveChart() {
openNewTab(
{
title: 'Chart #',
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid,
database,
},
},
{
editor: {
config: { chartType: 'bar' },
sql: display.getExportQuery(select => {
select.orderBy = null;
}),
},
}
);
}
function openQuery() {
openNewTab(
{
title: 'Query #',
icon: 'img sql-file',
tabComponent: 'QueryTab',
props: {
schemaName: display.baseTable.schemaName,
pureName: display.baseTable.pureName,
conid,
database,
},
},
{
editor: display.getExportQuery(),
}
);
}
function handleSave() {
const script = changeSetToSql(changeSetRef.current, display.dbinfo);
const sql = scriptToSql(display.driver, script);
setConfirmSql(sql);
confirmSqlModalState.open();
}
async function handleConfirmSql() {
const resp = await axios.request({
url: 'database-connections/query-data',
method: 'post',
params: {
conid,
database,
},
data: { sql: confirmSql },
});
const { errorMessage } = resp.data || {};
if (errorMessage) {
showModal(modalState => (
<ErrorMessageModal modalState={modalState} message={errorMessage} title="Error when saving" />
));
} else {
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
setConfirmSql(null);
display.reload();
}
}
// const grider = React.useMemo(()=>new ChangeSetGrider())
return (
<>
<LoadingDataGridCore
{...props}
exportGrid={exportGrid}
openActiveChart={openActiveChart}
openQuery={openQuery}
loadDataPage={loadDataPage}
dataPageAvailable={dataPageAvailable}
loadRowCount={loadRowCount}
griderFactory={ChangeSetGrider.factory}
griderFactoryDeps={ChangeSetGrider.factoryDeps}
// changeSet={changeSetState && changeSetState.value}
onSave={handleSave}
/>
<ConfirmSqlModal
modalState={confirmSqlModalState}
sql={confirmSql}
engine={display.engine}
onConfirm={handleConfirmSql}
/>
</>
);
}

View File

@@ -1,232 +0,0 @@
import React from 'react';
import _ from 'lodash';
import DataGrid from './DataGrid';
import styled from 'styled-components';
import { TableGridDisplay, TableFormViewDisplay, createGridConfig, createGridCache } from 'dbgate-datalib';
import { getFilterValueExpression } from 'dbgate-filterparser';
import { findEngineDriver } from 'dbgate-tools';
import { useConnectionInfo, getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import useSocket from '../utility/SocketProvider';
import { VerticalSplitter } from '../widgets/Splitter';
import stableStringify from 'json-stable-stringify';
import ReferenceHeader from './ReferenceHeader';
import SqlDataGridCore from './SqlDataGridCore';
import useExtensions from '../utility/useExtensions';
import SqlFormView from '../formview/SqlFormView';
const ReferenceContainer = styled.div`
position: absolute;
display: flex;
flex-direction: column;
top: 0;
left: 0;
right: 0;
bottom: 0;
`;
const ReferenceGridWrapper = styled.div`
position: relative;
flex: 1;
display: flex;
`;
export default function TableDataGrid({
conid,
database,
schemaName,
pureName,
tabVisible,
toolbarPortalRef,
changeSetState,
dispatchChangeSet,
config = undefined,
setConfig = undefined,
cache = undefined,
setCache = undefined,
masterLoadedTime = undefined,
isDetailView = false,
}) {
// const [childConfig, setChildConfig] = React.useState(createGridConfig());
const [myCache, setMyCache] = React.useState(createGridCache());
const [childCache, setChildCache] = React.useState(createGridCache());
const [refReloadToken, setRefReloadToken] = React.useState(0);
const [myLoadedTime, setMyLoadedTime] = React.useState(0);
const extensions = useExtensions();
const { childConfig } = config;
const setChildConfig = (value, reference = undefined) => {
if (_.isFunction(value)) {
setConfig(x => ({
...x,
childConfig: value(x.childConfig),
}));
} else {
setConfig(x => ({
...x,
childConfig: value,
reference: reference === undefined ? x.reference : reference,
}));
}
};
const { reference } = config;
const connection = useConnectionInfo({ conid });
const dbinfo = useDatabaseInfo({ conid, database });
// const [reference, setReference] = React.useState(null);
function createDisplay() {
return connection
? new TableGridDisplay(
{ schemaName, pureName },
findEngineDriver(connection, extensions),
config,
setConfig,
cache || myCache,
setCache || setMyCache,
dbinfo
)
: null;
}
function createFormDisplay() {
return connection
? new TableFormViewDisplay(
{ schemaName, pureName },
findEngineDriver(connection, extensions),
config,
setConfig,
cache || myCache,
setCache || setMyCache,
dbinfo
)
: null;
}
const [display, setDisplay] = React.useState(createDisplay());
const [formDisplay, setFormDisplay] = React.useState(createFormDisplay());
React.useEffect(() => {
setRefReloadToken(v => v + 1);
if (!reference && display && display.isGrouped) display.clearGrouping();
}, [reference]);
React.useEffect(() => {
const newDisplay = createDisplay();
if (!newDisplay) return;
if (display && display.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;
setDisplay(newDisplay);
}, [connection, config, cache || myCache, conid, database, schemaName, pureName, dbinfo, extensions]);
React.useEffect(() => {
const newDisplay = createFormDisplay();
if (!newDisplay) return;
if (formDisplay && formDisplay.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;
setFormDisplay(newDisplay);
}, [connection, config, cache || myCache, conid, database, schemaName, pureName, dbinfo, extensions]);
const handleDatabaseStructureChanged = React.useCallback(() => {
(setCache || setMyCache)(createGridCache());
}, []);
const socket = useSocket();
React.useEffect(() => {
if (display && !display.isLoadedCorrectly) {
if (conid && socket) {
socket.on(`database-structure-changed-${conid}-${database}`, handleDatabaseStructureChanged);
return () => {
socket.off(`database-structure-changed-${conid}-${database}`, handleDatabaseStructureChanged);
};
}
}
}, [conid, database, display]);
const handleReferenceSourceChanged = React.useCallback(
(selectedRows, loadedTime) => {
setMyLoadedTime(loadedTime);
if (!reference) return;
const filtersBase = display && display.isGrouped ? config.filters : childConfig.filters;
const filters = {
...filtersBase,
..._.fromPairs(
reference.columns.map(col => [
col.refName,
selectedRows.map(x => getFilterValueExpression(x[col.baseName], col.dataType)).join(', '),
])
),
};
if (stableStringify(filters) != stableStringify(childConfig.filters)) {
setChildConfig(cfg => ({
...cfg,
filters,
}));
setChildCache(ca => ({
...ca,
refreshTime: new Date().getTime(),
}));
}
},
[childConfig, reference]
);
const handleCloseReference = () => {
setChildConfig(null, null);
};
if (!display) return null;
return (
<VerticalSplitter>
<DataGrid
// key={`${conid}, ${database}, ${schemaName}, ${pureName}`}
config={config}
setConfig={setConfig}
conid={conid}
database={database}
display={display}
formDisplay={formDisplay}
tabVisible={tabVisible}
changeSetState={changeSetState}
dispatchChangeSet={dispatchChangeSet}
toolbarPortalRef={toolbarPortalRef}
showReferences
onReferenceClick={reference => setChildConfig(createGridConfig(), reference)}
onReferenceSourceChanged={reference ? handleReferenceSourceChanged : null}
refReloadToken={refReloadToken.toString()}
masterLoadedTime={masterLoadedTime}
GridCore={SqlDataGridCore}
FormView={SqlFormView}
isDetailView={isDetailView}
// tableInfo={
// dbinfo && dbinfo.tables && dbinfo.tables.find((x) => x.pureName == pureName && x.schemaName == schemaName)
// }
/>
{reference && (
<ReferenceContainer>
<ReferenceHeader reference={reference} onClose={handleCloseReference} />
<ReferenceGridWrapper>
<TableDataGrid
key={`${reference.schemaName}.${reference.pureName}`}
conid={conid}
database={database}
pureName={reference.pureName}
schemaName={reference.schemaName}
changeSetState={changeSetState}
dispatchChangeSet={dispatchChangeSet}
toolbarPortalRef={toolbarPortalRef}
tabVisible={false}
config={childConfig}
setConfig={setChildConfig}
cache={childCache}
setCache={setChildCache}
masterLoadedTime={myLoadedTime}
isDetailView
/>
</ReferenceGridWrapper>
</ReferenceContainer>
)}
</VerticalSplitter>
);
}

View File

@@ -1,144 +0,0 @@
import _ from 'lodash';
import { SeriesSizes } from './SeriesSizes';
import { CellAddress } from './selection';
import { GridDisplay } from 'dbgate-datalib';
import Grider from './Grider';
export function countColumnSizes(grider: Grider, columns, containerWidth, display: GridDisplay) {
const columnSizes = new SeriesSizes();
if (!grider || !columns) return columnSizes;
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
//return this.context.measureText(txt).width;
// console.log('countColumnSizes', loadedRows.length, containerWidth);
columnSizes.maxSize = (containerWidth * 2) / 3;
columnSizes.count = columns.length;
// columnSizes.setExtraordinaryIndexes(this.getHiddenColumnIndexes(), this.getFrozenColumnIndexes());
// console.log('display.hiddenColumnIndexes', display.hiddenColumnIndexes)
columnSizes.setExtraordinaryIndexes(display.hiddenColumnIndexes, []);
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
//this.columnSizes.PutSizeOverride(col, this.columns[col].Name.length * 8);
const column = columns[colIndex];
if (display.config.columnWidths[column.uniqueName]) {
columnSizes.putSizeOverride(colIndex, display.config.columnWidths[column.uniqueName]);
continue;
}
// if (column.columnClientObject != null && column.columnClientObject.notNull) context.font = "bold 14px Helvetica";
// else context.font = "14px Helvetica";
context.font = 'bold 14px Helvetica';
const text = column.headerText;
const headerWidth = context.measureText(text).width + 64;
// if (column.columnClientObject != null && column.columnClientObject.icon != null) headerWidth += 16;
// if (this.getFilterOnColumn(column.uniquePath)) headerWidth += 16;
// if (this.getSortOrder(column.uniquePath)) headerWidth += 16;
columnSizes.putSizeOverride(colIndex, headerWidth);
}
// let headerWidth = this.rowHeaderWidthDefault;
// if (this.rowCount) headerWidth = context.measureText(this.rowCount.toString()).width + 8;
// this.rowHeaderWidth = this.rowHeaderWidthDefault;
// if (headerWidth > this.rowHeaderWidth) this.rowHeaderWidth = headerWidth;
context.font = '14px Helvetica';
for (let rowIndex = 0; rowIndex < Math.min(grider.rowCount, 20); rowIndex += 1) {
const row = grider.getRowData(rowIndex);
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
const uqName = columns[colIndex].uniqueName;
if (display.config.columnWidths[uqName]) {
continue;
}
const text = row[uqName];
const width = context.measureText(text).width + 8;
// console.log('colName', colName, text, width);
columnSizes.putSizeOverride(colIndex, width);
// let colName = this.columns[colIndex].uniquePath;
// let text: string = row[colName].gridText;
// let width = context.measureText(text).width + 8;
// if (row[colName].dataPrefix) width += context.measureText(row[colName].dataPrefix).width + 3;
// this.columnSizes.putSizeOverride(colIndex, width);
}
}
// for (let modelIndex = 0; modelIndex < this.columns.length; modelIndex++) {
// let width = getHashValue(this.widthHashPrefix + this.columns[modelIndex].uniquePath);
// if (width) this.columnSizes.putSizeOverride(modelIndex, _.toNumber(width), true);
// }
columnSizes.buildIndex();
return columnSizes;
}
export function countVisibleRealColumns(columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns) {
const visibleColumnCount = columnSizes.getVisibleScrollCount(firstVisibleColumnScrollIndex, gridScrollAreaWidth);
// console.log('visibleColumnCount', visibleColumnCount);
// console.log('gridScrollAreaWidth', gridScrollAreaWidth);
const visibleRealColumnIndexes = [];
const modelIndexes = {};
/** @type {(import('dbgate-datalib').DisplayColumn & {widthPx: string; colIndex: number})[]} */
const realColumns = [];
// frozen columns
for (let colIndex = 0; colIndex < columnSizes.frozenCount; colIndex++) {
visibleRealColumnIndexes.push(colIndex);
}
// scroll columns
for (
let colIndex = firstVisibleColumnScrollIndex;
colIndex < firstVisibleColumnScrollIndex + visibleColumnCount;
colIndex++
) {
visibleRealColumnIndexes.push(colIndex + columnSizes.frozenCount);
}
// real columns
for (let colIndex of visibleRealColumnIndexes) {
let modelColumnIndex = columnSizes.realToModel(colIndex);
modelIndexes[colIndex] = modelColumnIndex;
let col = columns[modelColumnIndex];
if (!col) continue;
const widthNumber = columnSizes.getSizeByRealIndex(colIndex);
realColumns.push({
...col,
colIndex,
widthNumber,
widthPx: `${widthNumber}px`,
});
}
return realColumns;
}
export function filterCellForRow(cell, row: number): CellAddress | null {
return cell && (cell[0] == row || _.isString(cell[0])) ? cell : null;
}
export function filterCellsForRow(cells, row: number): CellAddress[] | null {
const res = (cells || []).filter(x => x[0] == row || _.isString(x[0]));
return res.length > 0 ? res : null;
}
export function cellIsSelected(row, col, selectedCells) {
if (!selectedCells) return false;
for (const [selectedRow, selectedCol] of selectedCells) {
if (row == selectedRow && col == selectedCol) return true;
if (selectedRow == 'header' && col == selectedCol) return true;
if (row == selectedRow && selectedCol == 'header') return true;
if (selectedRow == 'header' && selectedCol == 'header') return true;
}
return false;
}

View File

@@ -1,69 +0,0 @@
import _ from 'lodash';
export type CellAddress = [number | 'header' | 'filter' | undefined, number | 'header' | undefined];
export type RegularCellAddress = [number, number];
export const topLeftCell: CellAddress = [0, 0];
export const undefinedCell: CellAddress = [undefined, undefined];
export const nullCell: CellAddress = null;
export const emptyCellArray: CellAddress[] = [];
export function isRegularCell(cell: CellAddress): cell is RegularCellAddress {
if (!cell) return false;
const [row, col] = cell;
return _.isNumber(row) && _.isNumber(col);
}
export function getCellRange(a: CellAddress, b: CellAddress): CellAddress[] {
const [rowA, colA] = a;
const [rowB, colB] = b;
if (_.isNumber(rowA) && _.isNumber(colA) && _.isNumber(rowB) && _.isNumber(colB)) {
const rowMin = Math.min(rowA, rowB);
const rowMax = Math.max(rowA, rowB);
const colMin = Math.min(colA, colB);
const colMax = Math.max(colA, colB);
const res = [];
for (let row = rowMin; row <= rowMax; row++) {
for (let col = colMin; col <= colMax; col++) {
res.push([row, col]);
}
}
return res;
}
if (rowA == 'header' && rowB == 'header' && _.isNumber(colA) && _.isNumber(colB)) {
const colMin = Math.min(colA, colB);
const colMax = Math.max(colA, colB);
const res = [];
for (let col = colMin; col <= colMax; col++) {
res.push(['header', col]);
}
return res;
}
if (colA == 'header' && colB == 'header' && _.isNumber(rowA) && _.isNumber(rowB)) {
const rowMin = Math.min(rowA, rowB);
const rowMax = Math.max(rowA, rowB);
const res = [];
for (let row = rowMin; row <= rowMax; row++) {
res.push([row, 'header']);
}
return res;
}
if (colA == 'header' && colB == 'header' && rowA == 'header' && rowB == 'header') {
return [['header', 'header']];
}
return [];
}
export function convertCellAddress(row, col): CellAddress {
const rowNumber = parseInt(row);
const colNumber = parseInt(col);
return [_.isNaN(rowNumber) ? row : rowNumber, _.isNaN(colNumber) ? col : colNumber];
}
export function cellFromEvent(event): CellAddress {
const cell = event.target.closest('td');
if (!cell) return undefinedCell;
const col = cell.getAttribute('data-col');
const row = cell.getAttribute('data-row');
return convertCellAddress(row, col);
}

View File

@@ -1,46 +0,0 @@
import { GridDisplay, ChangeSet, GridReferenceDefinition } from 'dbgate-datalib';
import Grider from './Grider';
export interface DataGridProps {
display: GridDisplay;
tabVisible?: boolean;
changeSetState?: { value: ChangeSet };
dispatchChangeSet?: Function;
toolbarPortalRef?: any;
showReferences?: boolean;
onReferenceClick?: (def: GridReferenceDefinition) => void;
onReferenceSourceChanged?: Function;
refReloadToken?: string;
masterLoadedTime?: number;
managerSize?: number;
grider?: Grider;
conid?: string;
database?: string;
jslid?: string;
[field: string]: any;
}
// export interface DataGridCoreProps extends DataGridProps {
// rows: any[];
// loadNextData?: Function;
// exportGrid?: Function;
// openQuery?: Function;
// undo?: Function;
// redo?: Function;
// errorMessage?: string;
// isLoadedAll?: boolean;
// loadedTime?: any;
// allRowCount?: number;
// conid?: string;
// database?: string;
// insertedRowCount?: number;
// isLoading?: boolean;
// }
// export interface LoadingDataGridProps extends DataGridProps {
// conid?: string;
// database?: string;
// jslid?: string;
// }

View File

@@ -1,352 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import DesignerTable from './DesignerTable';
import uuidv1 from 'uuid/v1';
import _ from 'lodash';
import useTheme from '../theme/useTheme';
import DesignerReference from './DesignerReference';
import cleanupDesignColumns from './cleanupDesignColumns';
import { isConnectedByReference } from './designerTools';
import { getTableInfo } from '../utility/metadataLoaders';
const Wrapper = styled.div`
flex: 1;
background-color: ${props => props.theme.designer_background};
overflow: scroll;
`;
const Canvas = styled.div`
width: 3000px;
height: 3000px;
position: relative;
`;
const EmptyInfo = styled.div`
margin: 50px;
font-size: 20px;
`;
function fixPositions(tables) {
const minLeft = _.min(tables.map(x => x.left));
const minTop = _.min(tables.map(x => x.top));
if (minLeft < 0 || minTop < 0) {
const dLeft = minLeft < 0 ? -minLeft : 0;
const dTop = minTop < 0 ? -minTop : 0;
return tables.map(tbl => ({
...tbl,
left: tbl.left + dLeft,
top: tbl.top + dTop,
}));
}
return tables;
}
export default function Designer({ value, onChange, conid, database }) {
const { tables, references } = value || {};
const theme = useTheme();
const [sourceDragColumn, setSourceDragColumn] = React.useState(null);
const [targetDragColumn, setTargetDragColumn] = React.useState(null);
const domTablesRef = React.useRef({});
const wrapperRef = React.useRef();
const [changeToken, setChangeToken] = React.useState(0);
const handleDrop = e => {
var data = e.dataTransfer.getData('app_object_drag_data');
e.preventDefault();
if (!data) return;
const rect = e.target.getBoundingClientRect();
var json = JSON.parse(data);
const { objectTypeField } = json;
if (objectTypeField != 'tables' && objectTypeField != 'views') return;
json.designerId = uuidv1();
json.left = e.clientX - rect.left;
json.top = e.clientY - rect.top;
onChange(current => {
const foreignKeys = _.compact([
...(json.foreignKeys || []).map(fk => {
const tables = ((current || {}).tables || []).filter(
tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName
);
if (tables.length == 1)
return {
...fk,
sourceId: json.designerId,
targetId: tables[0].designerId,
};
return null;
}),
..._.flatten(
((current || {}).tables || []).map(tbl =>
(tbl.foreignKeys || []).map(fk => {
if (fk.refTableName == json.pureName && fk.refSchemaName == json.schemaName) {
return {
...fk,
sourceId: tbl.designerId,
targetId: json.designerId,
};
}
return null;
})
)
),
]);
return {
...current,
tables: [...((current || {}).tables || []), json],
references:
foreignKeys.length == 1
? [
...((current || {}).references || []),
{
designerId: uuidv1(),
sourceId: foreignKeys[0].sourceId,
targetId: foreignKeys[0].targetId,
joinType: 'INNER JOIN',
columns: foreignKeys[0].columns.map(col => ({
source: col.columnName,
target: col.refColumnName,
})),
},
]
: (current || {}).references,
};
});
};
const changeTable = React.useCallback(
table => {
onChange(current => ({
...current,
tables: fixPositions((current.tables || []).map(x => (x.designerId == table.designerId ? table : x))),
}));
},
[onChange]
);
const bringToFront = React.useCallback(
table => {
onChange(
current => ({
...current,
tables: [...(current.tables || []).filter(x => x.designerId != table.designerId), table],
}),
true
);
},
[onChange]
);
const removeTable = React.useCallback(
table => {
onChange(current => ({
...current,
tables: (current.tables || []).filter(x => x.designerId != table.designerId),
references: (current.references || []).filter(
x => x.sourceId != table.designerId && x.targetId != table.designerId
),
columns: (current.columns || []).filter(x => x.designerId != table.designerId),
}));
},
[onChange]
);
const changeReference = React.useCallback(
ref => {
onChange(current => ({
...current,
references: (current.references || []).map(x => (x.designerId == ref.designerId ? ref : x)),
}));
},
[onChange]
);
const removeReference = React.useCallback(
ref => {
onChange(current => ({
...current,
references: (current.references || []).filter(x => x.designerId != ref.designerId),
}));
},
[onChange]
);
const handleCreateReference = (source, target) => {
onChange(current => {
const existingReference = (current.references || []).find(
x =>
(x.sourceId == source.designerId && x.targetId == target.designerId) ||
(x.sourceId == target.designerId && x.targetId == source.designerId)
);
return {
...current,
references: existingReference
? current.references.map(ref =>
ref == existingReference
? {
...existingReference,
columns: [
...existingReference.columns,
existingReference.sourceId == source.designerId
? {
source: source.columnName,
target: target.columnName,
}
: {
source: target.columnName,
target: source.columnName,
},
],
}
: ref
)
: [
...(current.references || []),
{
designerId: uuidv1(),
sourceId: source.designerId,
targetId: target.designerId,
joinType: isConnectedByReference(current, source, target, null) ? 'CROSS JOIN' : 'INNER JOIN',
columns: [
{
source: source.columnName,
target: target.columnName,
},
],
},
],
};
});
};
const handleAddReferenceByColumn = async (designerId, foreignKey) => {
const toTable = await getTableInfo({
conid,
database,
pureName: foreignKey.refTableName,
schemaName: foreignKey.refSchemaName,
});
const newTableDesignerId = uuidv1();
onChange(current => {
const fromTable = (current.tables || []).find(x => x.designerId == designerId);
if (!fromTable) return;
return {
...current,
tables: [
...(current.tables || []),
{
...toTable,
left: fromTable.left + 300,
top: fromTable.top + 50,
designerId: newTableDesignerId,
},
],
references: [
...(current.references || []),
{
designerId: uuidv1(),
sourceId: fromTable.designerId,
targetId: newTableDesignerId,
joinType: 'INNER JOIN',
columns: foreignKey.columns.map(col => ({
source: col.columnName,
target: col.refColumnName,
})),
},
],
};
});
};
const handleSelectColumn = React.useCallback(
column => {
onChange(
current => ({
...current,
columns: (current.columns || []).find(
x => x.designerId == column.designerId && x.columnName == column.columnName
)
? current.columns
: [...cleanupDesignColumns(current.columns), _.pick(column, ['designerId', 'columnName'])],
}),
true
);
},
[onChange]
);
const handleChangeColumn = React.useCallback(
(column, changeFunc) => {
onChange(current => {
const currentColumns = (current || {}).columns || [];
const existing = currentColumns.find(
x => x.designerId == column.designerId && x.columnName == column.columnName
);
if (existing) {
return {
...current,
columns: currentColumns.map(x => (x == existing ? changeFunc(existing) : x)),
};
} else {
return {
...current,
columns: [
...cleanupDesignColumns(currentColumns),
changeFunc(_.pick(column, ['designerId', 'columnName'])),
],
};
}
});
},
[onChange]
);
// React.useEffect(() => {
// setTimeout(() => setChangeToken((x) => x + 1), 100);
// }, [value]);
return (
<Wrapper theme={theme}>
{(tables || []).length == 0 && <EmptyInfo>Drag &amp; drop tables or views from left panel here</EmptyInfo>}
<Canvas onDragOver={e => e.preventDefault()} onDrop={handleDrop} ref={wrapperRef}>
{(references || []).map(ref => (
<DesignerReference
key={ref.designerId}
changeToken={changeToken}
domTablesRef={domTablesRef}
reference={ref}
onChangeReference={changeReference}
onRemoveReference={removeReference}
designer={value}
/>
))}
{(tables || []).map(table => (
<DesignerTable
key={table.designerId}
sourceDragColumn={sourceDragColumn}
setSourceDragColumn={setSourceDragColumn}
targetDragColumn={targetDragColumn}
setTargetDragColumn={setTargetDragColumn}
onCreateReference={handleCreateReference}
onSelectColumn={handleSelectColumn}
onChangeColumn={handleChangeColumn}
onAddReferenceByColumn={handleAddReferenceByColumn}
table={table}
onChangeTable={changeTable}
onBringToFront={bringToFront}
onRemoveTable={removeTable}
setChangeToken={setChangeToken}
wrapperRef={wrapperRef}
onChangeDomTable={table => {
domTablesRef.current[table.designerId] = table;
}}
designer={value}
/>
))}
</Canvas>
</Wrapper>
);
}

View File

@@ -1,91 +0,0 @@
import _ from 'lodash';
import { dumpSqlSelect, Select, JoinType, Condition, Relation, mergeConditions, Source } from 'dbgate-sqltree';
import { EngineDriver } from 'dbgate-types';
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
import { findPrimaryTable, findConnectingReference, referenceIsJoin, referenceIsExists } from './designerTools';
export class DesignerComponent {
subComponents: DesignerComponent[] = [];
parentComponent: DesignerComponent;
parentReference: DesignerReferenceInfo;
tables: DesignerTableInfo[] = [];
nonPrimaryReferences: DesignerReferenceInfo[] = [];
get primaryTable() {
return this.tables[0];
}
get nonPrimaryTables() {
return this.tables.slice(1);
}
get nonPrimaryTablesAndReferences() {
return _.zip(this.nonPrimaryTables, this.nonPrimaryReferences);
}
get myAndParentTables() {
return [...this.parentTables, ...this.tables];
}
get parentTables() {
return this.parentComponent ? this.parentComponent.myAndParentTables : [];
}
get thisAndSubComponentsTables() {
return [...this.tables, ..._.flatten(this.subComponents.map(x => x.thisAndSubComponentsTables))];
}
}
export class DesignerComponentCreator {
toAdd: DesignerTableInfo[];
components: DesignerComponent[] = [];
constructor(public designer: DesignerInfo) {
this.toAdd = [...designer.tables];
while (this.toAdd.length > 0) {
const component = this.parseComponent(null);
this.components.push(component);
}
}
parseComponent(root) {
if (root == null) {
root = findPrimaryTable(this.toAdd);
}
if (!root) return null;
_.remove(this.toAdd, x => x == root);
const res = new DesignerComponent();
res.tables.push(root);
for (;;) {
let found = false;
for (const test of this.toAdd) {
const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsJoin);
if (ref) {
res.tables.push(test);
res.nonPrimaryReferences.push(ref);
_.remove(this.toAdd, x => x == test);
found = true;
break;
}
}
if (!found) break;
}
for (;;) {
let found = false;
for (const test of this.toAdd) {
const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsExists);
if (ref) {
const subComponent = this.parseComponent(test);
res.subComponents.push(subComponent);
subComponent.parentComponent = res;
subComponent.parentReference = ref;
found = true;
break;
}
}
if (!found) break;
}
return res;
}
}

View File

@@ -1,215 +0,0 @@
import _ from 'lodash';
import {
dumpSqlSelect,
Select,
JoinType,
Condition,
Relation,
mergeConditions,
Source,
ResultField,
} from 'dbgate-sqltree';
import { EngineDriver } from 'dbgate-types';
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
import { DesignerComponent } from './DesignerComponentCreator';
import {
getReferenceConditions,
referenceIsCrossJoin,
referenceIsConnecting,
mergeSelectsFromDesigner,
findQuerySource,
findDesignerFilterType,
} from './designerTools';
import { parseFilter } from 'dbgate-filterparser';
export class DesignerQueryDumper {
constructor(public designer: DesignerInfo, public components: DesignerComponent[]) {}
get topLevelTables(): DesignerTableInfo[] {
return _.flatten(this.components.map(x => x.tables));
}
dumpComponent(component: DesignerComponent) {
const select: Select = {
commandType: 'select',
from: {
name: component.primaryTable,
alias: component.primaryTable.alias,
relations: [],
},
};
for (const [table, ref] of component.nonPrimaryTablesAndReferences) {
select.from.relations.push({
name: table,
alias: table.alias,
joinType: ref.joinType as JoinType,
conditions: getReferenceConditions(ref, this.designer),
});
}
for (const subComponent of component.subComponents) {
const subQuery = this.dumpComponent(subComponent);
subQuery.selectAll = true;
select.where = mergeConditions(select.where, {
conditionType: subComponent.parentReference.joinType == 'WHERE NOT EXISTS' ? 'notExists' : 'exists',
subQuery,
});
}
if (component.parentReference) {
select.where = mergeConditions(select.where, {
conditionType: 'and',
conditions: getReferenceConditions(component.parentReference, this.designer),
});
// cross join conditions in subcomponents
for (const ref of this.designer.references || []) {
if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, component.tables, component.myAndParentTables)) {
select.where = mergeConditions(select.where, {
conditionType: 'and',
conditions: getReferenceConditions(ref, this.designer),
});
}
}
this.addConditions(select, component.tables);
}
return select;
}
addConditions(select: Select, tables: DesignerTableInfo[]) {
for (const column of this.designer.columns || []) {
if (!column.filter) continue;
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
if (!table) continue;
if (!tables.find(x => x.designerId == table.designerId)) continue;
const condition = parseFilter(column.filter, findDesignerFilterType(column, this.designer));
if (condition) {
select.where = mergeConditions(
select.where,
_.cloneDeepWith(condition, expr => {
if (expr.exprType == 'placeholder')
return {
exprType: 'column',
columnName: column.columnName,
source: findQuerySource(this.designer, column.designerId),
};
})
);
}
}
}
addGroupConditions(select: Select, tables: DesignerTableInfo[], selectIsGrouped: boolean) {
for (const column of this.designer.columns || []) {
if (!column.groupFilter) continue;
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
if (!table) continue;
if (!tables.find(x => x.designerId == table.designerId)) continue;
const condition = parseFilter(column.groupFilter, findDesignerFilterType(column, this.designer));
if (condition) {
select.having = mergeConditions(
select.having,
_.cloneDeepWith(condition, expr => {
if (expr.exprType == 'placeholder') {
return this.getColumnOutputExpression(column, selectIsGrouped);
}
})
);
}
}
}
getColumnOutputExpression(col, selectIsGrouped): ResultField {
const source = findQuerySource(this.designer, col.designerId);
const { columnName } = col;
let { alias } = col;
if (selectIsGrouped && !col.isGrouped) {
// use aggregate
const aggregate = col.aggregate == null || col.aggregate == '---' ? 'MAX' : col.aggregate;
if (!alias) alias = `${aggregate}(${columnName})`;
return {
exprType: 'call',
func: aggregate == 'COUNT DISTINCT' ? 'COUNT' : aggregate,
argsPrefix: aggregate == 'COUNT DISTINCT' ? 'DISTINCT' : null,
alias,
args: [
{
exprType: 'column',
columnName,
source,
},
],
};
} else {
return {
exprType: 'column',
columnName,
alias,
source,
};
}
}
run() {
let res: Select = null;
for (const component of this.components) {
const select = this.dumpComponent(component);
if (res == null) res = select;
else res = mergeSelectsFromDesigner(res, select);
}
// top level cross join conditions
const topLevelTables = this.topLevelTables;
for (const ref of this.designer.references || []) {
if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, topLevelTables, topLevelTables)) {
res.where = mergeConditions(res.where, {
conditionType: 'and',
conditions: getReferenceConditions(ref, this.designer),
});
}
}
const topLevelColumns = (this.designer.columns || []).filter(col =>
topLevelTables.find(tbl => tbl.designerId == col.designerId)
);
const selectIsGrouped = !!topLevelColumns.find(x => x.isGrouped || (x.aggregate && x.aggregate != '---'));
const outputColumns = topLevelColumns.filter(x => x.isOutput);
if (outputColumns.length == 0) {
res.selectAll = true;
} else {
res.columns = outputColumns.map(col => this.getColumnOutputExpression(col, selectIsGrouped));
}
const groupedColumns = topLevelColumns.filter(x => x.isGrouped);
if (groupedColumns.length > 0) {
res.groupBy = groupedColumns.map(col => ({
exprType: 'column',
columnName: col.columnName,
source: findQuerySource(this.designer, col.designerId),
}));
}
const orderColumns = _.sortBy(
topLevelColumns.filter(x => x.sortOrder),
x => Math.abs(x.sortOrder)
);
if (orderColumns.length > 0) {
res.orderBy = orderColumns.map(col => ({
exprType: 'column',
direction: col.sortOrder < 0 ? 'DESC' : 'ASC',
columnName: col.columnName,
source: findQuerySource(this.designer, col.designerId),
}));
}
this.addConditions(res, topLevelTables);
this.addGroupConditions(res, topLevelTables, selectIsGrouped);
return res;
}
}

View File

@@ -1,177 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import DomTableRef from './DomTableRef';
import _ from 'lodash';
import useTheme from '../theme/useTheme';
import { useShowMenu } from '../modals/showMenu';
import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu';
import { isConnectedByReference } from './designerTools';
const StyledSvg = styled.svg`
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
`;
const ReferenceWrapper = styled.div`
position: absolute;
border: 1px solid ${props => props.theme.designer_line};
background-color: ${props => props.theme.designer_background};
z-index: 900;
border-radius: 10px;
width: 32px;
height: 32px;
`;
const ReferenceText = styled.span`
position: relative;
float: left;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 900;
white-space: nowrap;
background-color: ${props => props.theme.designer_background};
`;
function ReferenceContextMenu({ remove, setJoinType, isConnected }) {
return (
<>
<DropDownMenuItem onClick={remove}>Remove</DropDownMenuItem>
{!isConnected && (
<>
<DropDownMenuDivider />
<DropDownMenuItem onClick={() => setJoinType('INNER JOIN')}>Set INNER JOIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('LEFT JOIN')}>Set LEFT JOIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('RIGHT JOIN')}>Set RIGHT JOIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('FULL OUTER JOIN')}>Set FULL OUTER JOIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('CROSS JOIN')}>Set CROSS JOIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('WHERE EXISTS')}>Set WHERE EXISTS</DropDownMenuItem>
<DropDownMenuItem onClick={() => setJoinType('WHERE NOT EXISTS')}>Set WHERE NOT EXISTS</DropDownMenuItem>
</>
)}
</>
);
}
export default function DesignerReference({
domTablesRef,
reference,
changeToken,
onRemoveReference,
onChangeReference,
designer,
}) {
const { designerId, sourceId, targetId, columns, joinType } = reference;
const theme = useTheme();
const showMenu = useShowMenu();
const domTables = domTablesRef.current;
/** @type {DomTableRef} */
const sourceTable = domTables[sourceId];
/** @type {DomTableRef} */
const targetTable = domTables[targetId];
if (!sourceTable || !targetTable) return null;
const sourceRect = sourceTable.getRect();
const targetRect = targetTable.getRect();
if (!sourceRect || !targetRect) return null;
const buswi = 10;
const extwi = 25;
const possibilities = [];
possibilities.push({ xsrc: sourceRect.left - buswi, dirsrc: -1, xdst: targetRect.left - buswi, dirdst: -1 });
possibilities.push({ xsrc: sourceRect.left - buswi, dirsrc: -1, xdst: targetRect.right + buswi, dirdst: 1 });
possibilities.push({ xsrc: sourceRect.right + buswi, dirsrc: 1, xdst: targetRect.left - buswi, dirdst: -1 });
possibilities.push({ xsrc: sourceRect.right + buswi, dirsrc: 1, xdst: targetRect.right + buswi, dirdst: 1 });
let minpos = _.minBy(possibilities, p => Math.abs(p.xsrc - p.xdst));
let srcY = _.mean(columns.map(x => sourceTable.getColumnY(x.source)));
let dstY = _.mean(columns.map(x => targetTable.getColumnY(x.target)));
if (columns.length == 0) {
srcY = sourceTable.getColumnY('');
dstY = targetTable.getColumnY('');
}
const src = { x: minpos.xsrc, y: srcY };
const dst = { x: minpos.xdst, y: dstY };
const lineStyle = { fill: 'none', stroke: theme.designer_line, strokeWidth: 2 };
const handleContextMenu = event => {
event.preventDefault();
showMenu(
event.pageX,
event.pageY,
<ReferenceContextMenu
remove={() => onRemoveReference({ designerId })}
isConnected={isConnectedByReference(designer, { designerId: sourceId }, { designerId: targetId }, reference)}
setJoinType={joinType => {
onChangeReference({
...reference,
joinType,
});
}}
/>
);
};
return (
<>
<StyledSvg>
<polyline
points={`
${src.x},${src.y}
${src.x + extwi * minpos.dirsrc},${src.y}
${dst.x + extwi * minpos.dirdst},${dst.y}
${dst.x},${dst.y}
`}
style={lineStyle}
/>
{columns.map((col, colIndex) => {
let y1 = sourceTable.getColumnY(col.source);
let y2 = targetTable.getColumnY(col.target);
return (
<React.Fragment key={colIndex}>
<polyline
points={`
${src.x},${src.y}
${src.x},${y1}
${src.x - buswi * minpos.dirsrc},${y1}
`}
style={lineStyle}
/>
<polyline
points={`
${dst.x},${dst.y}
${dst.x},${y2}
${dst.x - buswi * minpos.dirdst},${y2}
`}
style={lineStyle}
/>
</React.Fragment>
);
})}
</StyledSvg>
<ReferenceWrapper
theme={theme}
style={{
left: (src.x + extwi * minpos.dirsrc + dst.x + extwi * minpos.dirdst) / 2 - 16,
top: (src.y + dst.y) / 2 - 16,
}}
onContextMenu={handleContextMenu}
>
<ReferenceText theme={theme}>
{_.snakeCase(joinType || 'CROSS JOIN')
.replace('_', '\xa0')
.replace('_', '\xa0')}
</ReferenceText>
</ReferenceWrapper>
</>
);
}

View File

@@ -1,413 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { findForeignKeyForColumn } from 'dbgate-tools';
import ColumnLabel from '../datagrid/ColumnLabel';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import DomTableRef from './DomTableRef';
import _ from 'lodash';
import { CheckboxField } from '../utility/inputs';
import { useShowMenu } from '../modals/showMenu';
import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu';
import useShowModal from '../modals/showModal';
import InputTextModal from '../modals/InputTextModal';
const Wrapper = styled.div`
position: absolute;
// background-color: white;
background-color: ${props => props.theme.designtable_background};
border: 1px solid ${props => props.theme.border};
`;
const Header = styled.div`
font-weight: bold;
text-align: center;
padding: 2px;
background: ${props =>
// @ts-ignore
props.objectTypeField == 'views'
? props.theme.designtable_background_magenta[2]
: props.theme.designtable_background_blue[2]};
border-bottom: 1px solid ${props => props.theme.border};
cursor: pointer;
display: flex;
justify-content: space-between;
`;
const ColumnsWrapper = styled.div`
max-height: 400px;
overflow-y: auto;
width: calc(100% - 10px);
padding: 5px;
`;
const HeaderLabel = styled.div``;
const CloseWrapper = styled.div`
${props =>
`
background-color: ${props.theme.toolbar_background} ;
&:hover {
background-color: ${props.theme.toolbar_background2} ;
}
&:active:hover {
background-color: ${props.theme.toolbar_background3};
}
`}
`;
// &:hover {
// background-color: ${(props) => props.theme.designtable_background_gold[1]};
// }
const ColumnLine = styled.div`
${props =>
// @ts-ignore
!props.isDragSource &&
// @ts-ignore
!props.isDragTarget &&
`
&:hover {
background-color: ${props.theme.designtable_background_gold[1]};
}
`}
${props =>
// @ts-ignore
props.isDragSource &&
`
background-color: ${props.theme.designtable_background_cyan[2]};
`}
${props =>
// @ts-ignore
props.isDragTarget &&
`
background-color: ${props.theme.designtable_background_cyan[2]};
`}
`;
function TableContextMenu({ remove, setTableAlias, removeTableAlias }) {
return (
<>
<DropDownMenuItem onClick={remove}>Remove</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={setTableAlias}>Set table alias</DropDownMenuItem>
{!!removeTableAlias && <DropDownMenuItem onClick={removeTableAlias}>Remove table alias</DropDownMenuItem>}
</>
);
}
function ColumnContextMenu({ setSortOrder, addReference }) {
return (
<>
<DropDownMenuItem onClick={() => setSortOrder(1)}>Sort ascending</DropDownMenuItem>
<DropDownMenuItem onClick={() => setSortOrder(-1)}>Sort descending</DropDownMenuItem>
<DropDownMenuItem onClick={() => setSortOrder(0)}>Unsort</DropDownMenuItem>
{!!addReference && <DropDownMenuItem onClick={addReference}>Add reference</DropDownMenuItem>}
</>
);
}
function ColumnDesignerIcons({ column, designerId, designer }) {
const designerColumn = (designer.columns || []).find(
x => x.designerId == designerId && x.columnName == column.columnName
);
if (!designerColumn) return null;
return (
<>
{!!designerColumn.filter && <FontIcon icon="img filter" />}
{designerColumn.sortOrder > 0 && <FontIcon icon="img sort-asc" />}
{designerColumn.sortOrder < 0 && <FontIcon icon="img sort-desc" />}
{!!designerColumn.isGrouped && <FontIcon icon="img group" />}
</>
);
}
export default function DesignerTable({
table,
onChangeTable,
onBringToFront,
onRemoveTable,
onCreateReference,
onAddReferenceByColumn,
onSelectColumn,
onChangeColumn,
sourceDragColumn,
setSourceDragColumn,
targetDragColumn,
setTargetDragColumn,
onChangeDomTable,
wrapperRef,
setChangeToken,
designer,
}) {
const { pureName, columns, left, top, designerId, alias, objectTypeField } = table;
const [movingPosition, setMovingPosition] = React.useState(null);
const movingPositionRef = React.useRef(null);
const theme = useTheme();
const domObjectsRef = React.useRef({});
const showMenu = useShowMenu();
const showModal = useShowModal();
const moveStartXRef = React.useRef(null);
const moveStartYRef = React.useRef(null);
const handleMove = React.useCallback(e => {
let diffX = e.clientX - moveStartXRef.current;
let diffY = e.clientY - moveStartYRef.current;
moveStartXRef.current = e.clientX;
moveStartYRef.current = e.clientY;
movingPositionRef.current = {
left: (movingPositionRef.current.left || 0) + diffX,
top: (movingPositionRef.current.top || 0) + diffY,
};
setMovingPosition(movingPositionRef.current);
// setChangeToken((x) => x + 1);
changeTokenDebounced.current();
// onChangeTable(
// {
// ...props,
// left: (left || 0) + diffX,
// top: (top || 0) + diffY,
// },
// index
// );
}, []);
const changeTokenDebounced = React.useRef(
// @ts-ignore
_.debounce(() => setChangeToken(x => x + 1), 100)
);
const handleMoveEnd = React.useCallback(
e => {
if (movingPositionRef.current) {
onChangeTable({
...table,
left: movingPositionRef.current.left,
top: movingPositionRef.current.top,
});
}
movingPositionRef.current = null;
setMovingPosition(null);
changeTokenDebounced.current();
// setChangeToken((x) => x + 1);
// this.props.model.fixPositions();
// this.props.designer.changedModel(true);
},
[onChangeTable, table]
);
React.useEffect(() => {
if (movingPosition) {
document.addEventListener('mousemove', handleMove, true);
document.addEventListener('mouseup', handleMoveEnd, true);
return () => {
document.removeEventListener('mousemove', handleMove, true);
document.removeEventListener('mouseup', handleMoveEnd, true);
};
}
}, [movingPosition == null, handleMove, handleMoveEnd]);
const headerMouseDown = React.useCallback(
e => {
e.preventDefault();
moveStartXRef.current = e.clientX;
moveStartYRef.current = e.clientY;
movingPositionRef.current = { left, top };
setMovingPosition(movingPositionRef.current);
// setIsMoving(true);
},
[handleMove, handleMoveEnd]
);
const dispatchDomColumn = (columnName, dom) => {
domObjectsRef.current[columnName] = dom;
onChangeDomTable(new DomTableRef(table, domObjectsRef.current, wrapperRef.current));
changeTokenDebounced.current();
};
const handleSetTableAlias = () => {
showModal(modalState => (
<InputTextModal
modalState={modalState}
value={alias || ''}
label="New alias"
header="Set table alias"
onConfirm={newAlias => {
onChangeTable({
...table,
alias: newAlias,
});
}}
/>
));
};
const handleHeaderContextMenu = event => {
event.preventDefault();
showMenu(
event.pageX,
event.pageY,
<TableContextMenu
remove={() => onRemoveTable({ designerId })}
setTableAlias={handleSetTableAlias}
removeTableAlias={
alias
? () =>
onChangeTable({
...table,
alias: null,
})
: null
}
/>
);
};
const handleColumnContextMenu = column => event => {
event.preventDefault();
const foreignKey = findForeignKeyForColumn(table, column);
showMenu(
event.pageX,
event.pageY,
<ColumnContextMenu
setSortOrder={sortOrder => {
onChangeColumn(
{
...column,
designerId,
},
col => ({ ...col, sortOrder })
);
}}
addReference={
foreignKey
? () => {
onAddReferenceByColumn(designerId, foreignKey);
}
: null
}
/>
);
};
return (
<Wrapper
theme={theme}
style={{
left: movingPosition ? movingPosition.left : left,
top: movingPosition ? movingPosition.top : top,
}}
onMouseDown={() => onBringToFront(table)}
ref={dom => dispatchDomColumn('', dom)}
>
<Header
onMouseDown={headerMouseDown}
theme={theme}
onContextMenu={handleHeaderContextMenu}
// @ts-ignore
objectTypeField={objectTypeField}
>
<HeaderLabel>{alias || pureName}</HeaderLabel>
<CloseWrapper onClick={() => onRemoveTable(table)} theme={theme}>
<FontIcon icon="icon close" />
</CloseWrapper>
</Header>
<ColumnsWrapper>
{(columns || []).map(column => (
<ColumnLine
onContextMenu={handleColumnContextMenu(column)}
key={column.columnName}
theme={theme}
draggable
ref={dom => dispatchDomColumn(column.columnName, dom)}
// @ts-ignore
isDragSource={
sourceDragColumn &&
sourceDragColumn.designerId == designerId &&
sourceDragColumn.columnName == column.columnName
}
// @ts-ignore
isDragTarget={
targetDragColumn &&
targetDragColumn.designerId == designerId &&
targetDragColumn.columnName == column.columnName
}
onDragStart={e => {
const dragData = {
...column,
designerId,
};
setSourceDragColumn(dragData);
e.dataTransfer.setData('designer_column_drag_data', JSON.stringify(dragData));
}}
onDragEnd={e => {
setTargetDragColumn(null);
setSourceDragColumn(null);
}}
onDragOver={e => {
if (sourceDragColumn) {
e.preventDefault();
setTargetDragColumn({
...column,
designerId,
});
}
}}
onDrop={e => {
var data = e.dataTransfer.getData('designer_column_drag_data');
e.preventDefault();
if (!data) return;
onCreateReference(sourceDragColumn, targetDragColumn);
setTargetDragColumn(null);
setSourceDragColumn(null);
}}
onMouseDown={e =>
onSelectColumn({
...column,
designerId,
})
}
>
<CheckboxField
checked={
!!(designer.columns || []).find(
x => x.designerId == designerId && x.columnName == column.columnName && x.isOutput
)
}
onChange={e => {
if (e.target.checked) {
onChangeColumn(
{
...column,
designerId,
},
col => ({ ...col, isOutput: true })
);
} else {
onChangeColumn(
{
...column,
designerId,
},
col => ({ ...col, isOutput: false })
);
}
}}
/>
<ColumnLabel {...column} foreignKey={findForeignKeyForColumn(table, column)} forceIcon />
<ColumnDesignerIcons column={column} designerId={designerId} designer={designer} />
</ColumnLine>
))}
</ColumnsWrapper>
</Wrapper>
);
}

View File

@@ -1,39 +0,0 @@
import { DesignerTableInfo } from './types';
export default class DomTableRef {
domTable: Element;
domWrapper: Element;
table: DesignerTableInfo;
designerId: string;
domRefs: { [column: string]: Element };
constructor(table: DesignerTableInfo, domRefs, domWrapper: Element) {
this.domTable = domRefs[''];
this.domWrapper = domWrapper;
this.table = table;
this.designerId = table.designerId;
this.domRefs = domRefs;
}
getRect() {
if (!this.domWrapper) return null;
if (!this.domTable) return null;
const wrap = this.domWrapper.getBoundingClientRect();
const rect = this.domTable.getBoundingClientRect();
return {
left: rect.left - wrap.left,
top: rect.top - wrap.top,
right: rect.right - wrap.left,
bottom: rect.bottom - wrap.top,
};
}
getColumnY(columnName: string) {
let col = this.domRefs[columnName];
if (!col) return null;
const rect = col.getBoundingClientRect();
const wrap = this.domWrapper.getBoundingClientRect();
return (rect.top + rect.bottom) / 2 - wrap.top;
}
}

View File

@@ -1,165 +0,0 @@
import React from 'react';
import DataFilterControl from '../datagrid/DataFilterControl';
import { CheckboxField, SelectField, TextField } from '../utility/inputs';
import TableControl, { TableColumn } from '../utility/TableControl';
import InlineButton from '../widgets/InlineButton';
import { findDesignerFilterType } from './designerTools';
function getTableDisplayName(column, tables) {
const table = (tables || []).find(x => x.designerId == column.designerId);
if (table) return table.alias || table.pureName;
return '';
}
export default function QueryDesignColumns({ value, onChange }) {
const { columns, tables } = value || {};
const changeColumn = React.useCallback(
col => {
onChange(current => ({
...current,
columns: (current.columns || []).map(x =>
x.designerId == col.designerId && x.columnName == col.columnName ? col : x
),
}));
},
[onChange]
);
const removeColumn = React.useCallback(
col => {
onChange(current => ({
...current,
columns: (current.columns || []).filter(x => x.designerId != col.designerId || x.columnName != col.columnName),
}));
},
[onChange]
);
const hasGroupedColumn = !!(columns || []).find(x => x.isGrouped);
return (
<TableControl rows={columns || []}>
<TableColumn fieldName="columnName" header="Column/Expression" />
<TableColumn fieldName="tableDisplayName" header="Table" formatter={row => getTableDisplayName(row, tables)} />
<TableColumn
fieldName="isOutput"
header="Output"
formatter={row => (
<CheckboxField
checked={row.isOutput}
onChange={e => {
if (e.target.checked) changeColumn({ ...row, isOutput: true });
else changeColumn({ ...row, isOutput: false });
}}
/>
)}
/>
<TableColumn
fieldName="alias"
header="Alias"
formatter={row => (
<TextField
value={row.alias}
onChange={e => {
changeColumn({ ...row, alias: e.target.value });
}}
/>
)}
/>
<TableColumn
fieldName="isGrouped"
header="Group by"
formatter={row => (
<CheckboxField
checked={row.isGrouped}
onChange={e => {
if (e.target.checked) changeColumn({ ...row, isGrouped: true });
else changeColumn({ ...row, isGrouped: false });
}}
/>
)}
/>
<TableColumn
fieldName="aggregate"
header="Aggregate"
formatter={row =>
!row.isGrouped && (
<SelectField
value={row.aggregate}
onChange={e => {
changeColumn({ ...row, aggregate: e.target.value });
}}
>
<option value="---">---</option>
<option value="MIN">MIN</option>
<option value="MAX">MAX</option>
<option value="COUNT">COUNT</option>
<option value="COUNT DISTINCT">COUNT DISTINCT</option>
<option value="SUM">SUM</option>
<option value="AVG">AVG</option>
</SelectField>
)
}
/>
<TableColumn
fieldName="sortOrder"
header="Sort order"
formatter={row => (
<SelectField
value={row.sortOrder}
onChange={e => {
changeColumn({ ...row, sortOrder: parseInt(e.target.value) });
}}
>
<option value="0">---</option>
<option value="1">1st, ascending</option>
<option value="-1">1st, descending</option>
<option value="2">2nd, ascending</option>
<option value="-2">2nd, descending</option>
<option value="3">3rd, ascending</option>
<option value="-3">3rd, descending</option>,
</SelectField>
)}
/>
<TableColumn
fieldName="filter"
header="Filter"
formatter={row => (
<DataFilterControl
filterType={findDesignerFilterType(row, value)}
filter={row.filter}
setFilter={filter => {
changeColumn({ ...row, filter });
}}
/>
)}
/>
{hasGroupedColumn && (
<TableColumn
fieldName="groupFilter"
header="Group filter"
formatter={row => (
<DataFilterControl
filterType={findDesignerFilterType(row, value)}
filter={row.groupFilter}
setFilter={groupFilter => {
changeColumn({ ...row, groupFilter });
}}
/>
)}
/>
)}
<TableColumn
fieldName="actions"
header=""
formatter={row => (
<>
<InlineButton onClick={() => removeColumn(row)}>Remove</InlineButton>
</>
)}
/>
</TableControl>
);
}

View File

@@ -1,29 +0,0 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
export default function QueryDesignToolbar({
execute,
isDatabaseDefined,
busy,
modelState,
dispatchModel,
isConnected,
kill,
}) {
return (
<>
<ToolbarButton disabled={!isDatabaseDefined || busy} onClick={execute} icon="icon run">
Execute
</ToolbarButton>
<ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close">
Kill
</ToolbarButton>
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo
</ToolbarButton>
<ToolbarButton disabled={!modelState.canRedo} onClick={() => dispatchModel({ type: 'redo' })} icon="icon redo">
Redo
</ToolbarButton>
</>
);
}

View File

@@ -1,7 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import Designer from './Designer';
export default function QueryDesigner({ value, conid, database, engine, onChange }) {
return <Designer value={value} onChange={onChange} conid={conid} database={database}></Designer>;
}

View File

@@ -1,5 +0,0 @@
export default function cleanupDesignColumns(columns) {
return (columns || []).filter(
x => x.isOutput || x.isGrouped || x.alias || (x.aggregate && x.aggregate != '---') || x.sortOrder || x.filter
);
}

View File

@@ -1,144 +0,0 @@
import _ from 'lodash';
import { dumpSqlSelect, Select, JoinType, Condition, Relation, mergeConditions, Source } from 'dbgate-sqltree';
import { EngineDriver } from 'dbgate-types';
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
import { DesignerComponentCreator } from './DesignerComponentCreator';
import { DesignerQueryDumper } from './DesignerQueryDumper';
import { getFilterType } from 'dbgate-filterparser';
export function referenceIsConnecting(
reference: DesignerReferenceInfo,
tables1: DesignerTableInfo[],
tables2: DesignerTableInfo[]
) {
return (
(tables1.find(x => x.designerId == reference.sourceId) && tables2.find(x => x.designerId == reference.targetId)) ||
(tables1.find(x => x.designerId == reference.targetId) && tables2.find(x => x.designerId == reference.sourceId))
);
}
export function referenceIsJoin(reference) {
return ['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL OUTER JOIN'].includes(reference.joinType);
}
export function referenceIsExists(reference) {
return ['WHERE EXISTS', 'WHERE NOT EXISTS'].includes(reference.joinType);
}
export function referenceIsCrossJoin(reference) {
return !reference.joinType || reference.joinType == 'CROSS JOIN';
}
export function findConnectingReference(
designer: DesignerInfo,
tables1: DesignerTableInfo[],
tables2: DesignerTableInfo[],
additionalCondition: (ref: DesignerReferenceInfo) => boolean
) {
for (const ref of designer.references || []) {
if (additionalCondition(ref) && referenceIsConnecting(ref, tables1, tables2)) {
return ref;
}
}
return null;
}
export function findQuerySource(designer: DesignerInfo, designerId: string): Source {
const table = designer.tables.find(x => x.designerId == designerId);
if (!table) return null;
return {
name: table,
alias: table.alias,
};
}
export function mergeSelectsFromDesigner(select1: Select, select2: Select): Select {
return {
commandType: 'select',
from: {
...select1.from,
relations: [
...select1.from.relations,
{
joinType: 'CROSS JOIN',
name: select2.from.name,
alias: select2.from.alias,
},
...select2.from.relations,
],
},
where: mergeConditions(select1.where, select2.where),
};
}
export function findPrimaryTable(tables: DesignerTableInfo[]) {
return _.minBy(tables, x => x.top);
}
export function getReferenceConditions(reference: DesignerReferenceInfo, designer: DesignerInfo): Condition[] {
const sourceTable = designer.tables.find(x => x.designerId == reference.sourceId);
const targetTable = designer.tables.find(x => x.designerId == reference.targetId);
return reference.columns.map(col => ({
conditionType: 'binary',
operator: '=',
left: {
exprType: 'column',
columnName: col.source,
source: {
name: sourceTable,
alias: sourceTable.alias,
},
},
right: {
exprType: 'column',
columnName: col.target,
source: {
name: targetTable,
alias: targetTable.alias,
},
},
}));
}
export function generateDesignedQuery(designer: DesignerInfo, engine: EngineDriver) {
const { tables, columns, references } = designer;
const primaryTable = findPrimaryTable(designer.tables);
if (!primaryTable) return '';
const componentCreator = new DesignerComponentCreator(designer);
const designerDumper = new DesignerQueryDumper(designer, componentCreator.components);
const select = designerDumper.run();
const dmp = engine.createDumper();
dumpSqlSelect(dmp, select);
return dmp.s;
}
export function isConnectedByReference(
designer: DesignerInfo,
table1: { designerId: string },
table2: { designerId: string },
withoutRef: { designerId: string }
) {
if (!designer.references) return false;
const creator = new DesignerComponentCreator({
...designer,
references: withoutRef
? designer.references.filter(x => x.designerId != withoutRef.designerId)
: designer.references,
});
const arrays = creator.components.map(x => x.thisAndSubComponentsTables);
const array1 = arrays.find(a => a.find(x => x.designerId == table1.designerId));
const array2 = arrays.find(a => a.find(x => x.designerId == table2.designerId));
return array1 == array2;
}
export function findDesignerFilterType({ designerId, columnName }, designer) {
const table = (designer.tables || []).find(x => x.designerId == designerId);
if (table) {
const column = (table.columns || []).find(x => x.columnName == columnName);
if (column) {
const { dataType } = column;
return getFilterType(dataType);
}
}
return 'string';
}

View File

@@ -1,44 +0,0 @@
import { JoinType } from 'dbgate-sqltree';
import { TableInfo } from 'dbgate-types';
export type DesignerTableInfo = TableInfo & {
designerId: string;
alias?: string;
left: number;
top: number;
};
export type DesignerJoinType = JoinType | 'WHERE EXISTS' | 'WHERE NOT EXISTS';
export type DesignerReferenceInfo = {
designerId: string;
joinType: DesignerJoinType;
sourceId: string;
targetId: string;
columns: {
source: string;
target: string;
}[];
};
export type DesignerColumnInfo = {
designerId: string;
columnName: string;
alias?: string;
isGrouped?: boolean;
aggregate?: string;
isOutput?: boolean;
sortOrder?: number;
filter?: string;
groupFilter?: string;
};
export type DesignerInfo = {
tables: DesignerTableInfo[];
columns: DesignerColumnInfo[];
references: DesignerReferenceInfo[];
};
// export type DesignerComponent = {
// tables: DesignerTableInfo[];
// };

View File

@@ -1,93 +0,0 @@
import {
ChangeSet,
changeSetContainsChanges,
changeSetInsertNewRow,
createChangeSet,
deleteChangeSetRows,
findExistingChangeSetItem,
getChangeSetInsertedRows,
TableFormViewDisplay,
revertChangeSetRowChanges,
setChangeSetValue,
ChangeSetRowDefinition,
} from 'dbgate-datalib';
import Former from './Former';
export default class ChangeSetFormer extends Former {
public changeSet: ChangeSet;
public setChangeSet: Function;
private batchChangeSet: ChangeSet;
public rowDefinition: ChangeSetRowDefinition;
public rowStatus;
constructor(
public sourceRow: any,
public changeSetState,
public dispatchChangeSet,
public display: TableFormViewDisplay
) {
super();
this.changeSet = changeSetState && changeSetState.value;
this.setChangeSet = value => dispatchChangeSet({ type: 'set', value });
this.batchChangeSet = null;
this.rowDefinition = display.getChangeSetRow(sourceRow);
const [matchedField, matchedChangeSetItem] = findExistingChangeSetItem(this.changeSet, this.rowDefinition);
this.rowData = matchedChangeSetItem ? { ...sourceRow, ...matchedChangeSetItem.fields } : sourceRow;
let status = 'regular';
if (matchedChangeSetItem && matchedField == 'updates') status = 'updated';
if (matchedField == 'deletes') status = 'deleted';
this.rowStatus = {
status,
modifiedFields:
matchedChangeSetItem && matchedChangeSetItem.fields ? new Set(Object.keys(matchedChangeSetItem.fields)) : null,
};
}
applyModification(changeSetReducer) {
if (this.batchChangeSet) {
this.batchChangeSet = changeSetReducer(this.batchChangeSet);
} else {
this.setChangeSet(changeSetReducer(this.changeSet));
}
}
setCellValue(uniqueName: string, value: any) {
const row = this.sourceRow;
const definition = this.display.getChangeSetField(row, uniqueName);
this.applyModification(chs => setChangeSetValue(chs, definition, value));
}
deleteRow(index: number) {
this.applyModification(chs => deleteChangeSetRows(chs, this.rowDefinition));
}
beginUpdate() {
this.batchChangeSet = this.changeSet;
}
endUpdate() {
this.setChangeSet(this.batchChangeSet);
this.batchChangeSet = null;
}
revertRowChanges() {
this.applyModification(chs => revertChangeSetRowChanges(chs, this.rowDefinition));
}
revertAllChanges() {
this.applyModification(chs => createChangeSet());
}
undo() {
this.dispatchChangeSet({ type: 'undo' });
}
redo() {
this.dispatchChangeSet({ type: 'redo' });
}
get canUndo() {
return this.changeSetState.canUndo;
}
get canRedo() {
return this.changeSetState.canRedo;
}
get containsChanges() {
return changeSetContainsChanges(this.changeSet);
}
}

View File

@@ -1,583 +0,0 @@
// @ts-nocheck
import _ from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import ColumnLabel from '../datagrid/ColumnLabel';
import { findForeignKeyForColumn } from 'dbgate-tools';
import styled from 'styled-components';
import useTheme from '../theme/useTheme';
import useDimensions from '../utility/useDimensions';
import FormViewToolbar from './FormViewToolbar';
import { useShowMenu } from '../modals/showMenu';
import FormViewContextMenu from './FormViewContextMenu';
import keycodes from '../utility/keycodes';
import { CellFormattedValue, ShowFormButton } from '../datagrid/DataGridRow';
import { cellFromEvent } from '../datagrid/selection';
import InplaceEditor from '../datagrid/InplaceEditor';
import { copyTextToClipboard } from '../utility/clipboard';
import { ExpandIcon, FontIcon } from '../icons';
import openReferenceForm from './openReferenceForm';
import useOpenNewTab from '../utility/useOpenNewTab';
import LoadingInfo from '../widgets/LoadingInfo';
const Table = styled.table`
border-collapse: collapse;
outline: none;
`;
const OuterWrapper = styled.div`
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
`;
const Wrapper = styled.div`
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
display: flex;
overflow-x: scroll;
`;
const TableRow = styled.tr`
background-color: ${props => props.theme.gridbody_background};
&:nth-child(6n + 3) {
background-color: ${props => props.theme.gridbody_background_alt2};
}
&:nth-child(6n + 6) {
background-color: ${props => props.theme.gridbody_background_alt3};
}
`;
const TableHeaderCell = styled.td`
border: 1px solid ${props => props.theme.border};
text-align: left;
padding: 2px;
background-color: ${props => props.theme.gridheader_background};
overflow: hidden;
position: relative;
${props =>
props.isSelected &&
`
background: initial;
background-color: ${props.theme.gridbody_selection[4]};
color: ${props.theme.gridbody_invfont1};`}
`;
const TableBodyCell = styled.td`
font-weight: normal;
border: 1px solid ${props => props.theme.border};
// border-collapse: collapse;
padding: 2px;
white-space: nowrap;
position: relative;
max-width: 500px;
overflow: hidden;
text-overflow: ellipsis;
${props =>
props.isSelected &&
`
background: initial;
background-color: ${props.theme.gridbody_selection[4]};
color: ${props.theme.gridbody_invfont1};`}
${props =>
!props.isSelected &&
props.isModifiedCell &&
`
background-color: ${props.theme.gridbody_background_orange[1]};`}
`;
const FocusField = styled.input`
// visibility: hidden
position: absolute;
left: -1000px;
top: -1000px;
`;
const RowCountLabel = styled.div`
position: absolute;
background-color: ${props => props.theme.gridbody_background_yellow[1]};
right: 40px;
bottom: 20px;
`;
const HintSpan = styled.span`
color: gray;
margin-left: 5px;
margin-right: 16px;
`;
const ColumnLabelMargin = styled(ColumnLabel)`
margin-right: 16px;
`;
function isDataCell(cell) {
return cell[1] % 2 == 1;
}
export default function FormView(props) {
const {
toolbarPortalRef,
tabVisible,
config,
setConfig,
onNavigate,
former,
onSave,
conid,
database,
onReload,
onReconnect,
allRowCount,
rowCountBefore,
onSelectionChanged,
isLoading,
} = props;
/** @type {import('dbgate-datalib').FormViewDisplay} */
const formDisplay = props.formDisplay;
const theme = useTheme();
const [headerRowRef, { height: rowHeight }] = useDimensions();
const [wrapperRef, { height: wrapperHeight }] = useDimensions();
const showMenu = useShowMenu();
const focusFieldRef = React.useRef(null);
const [currentCell, setCurrentCell] = React.useState([0, 0]);
const cellRefs = React.useRef({});
const openNewTab = useOpenNewTab();
const rowCount = Math.floor((wrapperHeight - 20) / rowHeight);
const columnChunks = _.chunk(formDisplay.columns, rowCount);
const { rowData, rowStatus } = former;
const handleSwitchToTable = () => {
setConfig(cfg => ({
...cfg,
isFormView: false,
formViewKey: null,
}));
};
const handleFilterThisValue = isDataCell(currentCell)
? () => formDisplay.filterCellValue(getCellColumn(currentCell), rowData)
: null;
const handleContextMenu = event => {
event.preventDefault();
showMenu(
event.pageX,
event.pageY,
<FormViewContextMenu
switchToTable={handleSwitchToTable}
onNavigate={onNavigate}
addToFilter={() => formDisplay.addFilterColumn(getCellColumn(currentCell))}
filterThisValue={handleFilterThisValue}
/>
);
};
const setCellRef = (row, col, element) => {
cellRefs.current[`${row},${col}`] = element;
};
React.useEffect(() => {
if (tabVisible) {
if (focusFieldRef.current) focusFieldRef.current.focus();
}
}, [tabVisible, focusFieldRef.current]);
React.useEffect(() => {
if (!onSelectionChanged || !rowData) return;
const col = getCellColumn(currentCell);
if (!col) return;
onSelectionChanged(rowData[col.uniqueName]);
}, [onSelectionChanged, currentCell, rowData]);
const checkMoveCursorBounds = (row, col) => {
if (row < 0) row = 0;
if (col < 0) col = 0;
if (col >= columnChunks.length * 2) col = columnChunks.length * 2 - 1;
const chunk = columnChunks[Math.floor(col / 2)];
if (chunk && row >= chunk.length) row = chunk.length - 1;
return [row, col];
};
const handleCursorMove = event => {
if (event.ctrlKey) {
switch (event.keyCode) {
case keycodes.leftArrow:
return checkMoveCursorBounds(currentCell[0], 0);
case keycodes.rightArrow:
return checkMoveCursorBounds(currentCell[0], columnChunks.length * 2 - 1);
}
}
switch (event.keyCode) {
case keycodes.leftArrow:
return checkMoveCursorBounds(currentCell[0], currentCell[1] - 1);
case keycodes.rightArrow:
return checkMoveCursorBounds(currentCell[0], currentCell[1] + 1);
case keycodes.upArrow:
return checkMoveCursorBounds(currentCell[0] - 1, currentCell[1]);
case keycodes.downArrow:
return checkMoveCursorBounds(currentCell[0] + 1, currentCell[1]);
case keycodes.pageUp:
return checkMoveCursorBounds(0, currentCell[1]);
case keycodes.pageDown:
return checkMoveCursorBounds(rowCount - 1, currentCell[1]);
case keycodes.home:
return checkMoveCursorBounds(0, 0);
case keycodes.end:
return checkMoveCursorBounds(rowCount - 1, columnChunks.length * 2 - 1);
}
};
const handleKeyNavigation = event => {
if (event.ctrlKey) {
switch (event.keyCode) {
case keycodes.upArrow:
return 'previous';
case keycodes.downArrow:
return 'next';
case keycodes.home:
return 'begin';
case keycodes.end:
return 'end';
}
}
};
function handleSave() {
if (inplaceEditorState.cell) {
// @ts-ignore
dispatchInsplaceEditor({ type: 'shouldSave' });
return;
}
if (onSave) onSave();
}
function getCellColumn(cell) {
const chunk = columnChunks[Math.floor(cell[1] / 2)];
if (!chunk) return;
const column = chunk[cell[0]];
return column;
}
function setCellValue(cell, value) {
const column = getCellColumn(cell);
if (!column) return;
former.setCellValue(column.uniqueName, value);
}
function setNull() {
if (isDataCell(currentCell)) {
setCellValue(currentCell, null);
}
}
const scrollIntoView = cell => {
const element = cellRefs.current[`${cell[0]},${cell[1]}`];
if (element) element.scrollIntoView();
};
React.useEffect(() => {
scrollIntoView(currentCell);
}, [rowData]);
const moveCurrentCell = (row, col) => {
const moved = checkMoveCursorBounds(row, col);
setCurrentCell(moved);
scrollIntoView(moved);
};
function copyToClipboard() {
const column = getCellColumn(currentCell);
if (!column) return;
const text = currentCell[1] % 2 == 1 ? rowData[column.uniqueName] : column.columnName;
copyTextToClipboard(text);
}
const handleKeyDown = event => {
const navigation = handleKeyNavigation(event);
if (navigation) {
event.preventDefault();
onNavigate(navigation);
return;
}
const moved = handleCursorMove(event);
if (moved) {
setCurrentCell(moved);
scrollIntoView(moved);
event.preventDefault();
return;
}
if (event.keyCode == keycodes.s && event.ctrlKey) {
event.preventDefault();
handleSave();
// this.saveAndFocus();
}
if (event.keyCode == keycodes.n0 && event.ctrlKey) {
event.preventDefault();
setNull();
}
if (event.keyCode == keycodes.r && event.ctrlKey) {
event.preventDefault();
former.revertRowChanges();
}
// if (event.keyCode == keycodes.f && event.ctrlKey) {
// event.preventDefault();
// filterSelectedValue();
// }
if (event.keyCode == keycodes.z && event.ctrlKey) {
event.preventDefault();
former.undo();
}
if (event.keyCode == keycodes.y && event.ctrlKey) {
event.preventDefault();
former.redo();
}
if (event.keyCode == keycodes.c && event.ctrlKey) {
event.preventDefault();
copyToClipboard();
}
if (event.keyCode == keycodes.f && event.ctrlKey) {
event.preventDefault();
if (handleFilterThisValue) handleFilterThisValue();
}
if (event.keyCode == keycodes.f5) {
event.preventDefault();
onReload();
}
if (event.keyCode == keycodes.f4) {
event.preventDefault();
handleSwitchToTable();
}
if (
rowData &&
!event.ctrlKey &&
!event.altKey &&
((event.keyCode >= keycodes.a && event.keyCode <= keycodes.z) ||
(event.keyCode >= keycodes.n0 && event.keyCode <= keycodes.n9) ||
event.keyCode == keycodes.dash)
) {
// @ts-ignore
dispatchInsplaceEditor({ type: 'show', text: event.nativeEvent.key, cell: currentCell });
return;
}
if (rowData && event.keyCode == keycodes.f2) {
// @ts-ignore
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
return;
}
};
const handleTableMouseDown = event => {
event.preventDefault();
if (focusFieldRef.current) focusFieldRef.current.focus();
if (event.target.closest('.buttonLike')) return;
if (event.target.closest('.resizeHandleControl')) return;
if (event.target.closest('input')) return;
// event.target.closest('table').focus();
event.preventDefault();
if (focusFieldRef.current) focusFieldRef.current.focus();
const cell = cellFromEvent(event);
if (isDataCell(cell) && !_.isEqual(cell, inplaceEditorState.cell) && _.isEqual(cell, currentCell)) {
// @ts-ignore
if (rowData) {
dispatchInsplaceEditor({ type: 'show', cell, selectAll: true });
}
} else if (!_.isEqual(cell, inplaceEditorState.cell)) {
// @ts-ignore
dispatchInsplaceEditor({ type: 'close' });
}
// @ts-ignore
setCurrentCell(cell);
};
const getCellWidth = (row, col) => {
const element = cellRefs.current[`${row},${col}`];
if (element) return element.getBoundingClientRect().width;
return 100;
};
const rowCountInfo = React.useMemo(() => {
if (rowData == null) return 'No data';
if (allRowCount == null || rowCountBefore == null) return 'Loading row count...';
return `Row: ${(rowCountBefore + 1).toLocaleString()} / ${allRowCount.toLocaleString()}`;
}, [rowCountBefore, allRowCount]);
const [inplaceEditorState, dispatchInsplaceEditor] = React.useReducer((state, action) => {
switch (action.type) {
case 'show': {
const column = getCellColumn(action.cell);
if (!column) return state;
if (column.uniquePath.length > 1) return state;
// if (!grider.editable) return {};
return {
cell: action.cell,
text: action.text,
selectAll: action.selectAll,
};
}
case 'close': {
const [row, col] = currentCell || [];
if (focusFieldRef.current) focusFieldRef.current.focus();
// @ts-ignore
if (action.mode == 'enter' && row) setTimeout(() => moveCurrentCell(row + 1, col), 0);
// if (action.mode == 'save') setTimeout(handleSave, 0);
return {};
}
case 'shouldSave': {
return {
...state,
shouldSave: true,
};
}
}
return {};
}, {});
const toolbar =
toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<FormViewToolbar
switchToTable={handleSwitchToTable}
onNavigate={onNavigate}
reload={onReload}
reconnect={onReconnect}
save={handleSave}
former={former}
/>,
toolbarPortalRef.current
);
if (isLoading) {
return (
<>
<LoadingInfo wrapper message="Loading data" />
{toolbar}
</>
);
}
if (!formDisplay || !formDisplay.isLoadedCorrectly) return toolbar;
return (
<OuterWrapper>
<Wrapper ref={wrapperRef} onContextMenu={handleContextMenu}>
{columnChunks.map((chunk, chunkIndex) => (
<Table key={chunkIndex} onMouseDown={handleTableMouseDown}>
{chunk.map((col, rowIndex) => (
<TableRow key={col.uniqueName} theme={theme} ref={headerRowRef} style={{ height: `${rowHeight}px` }}>
<TableHeaderCell
theme={theme}
data-row={rowIndex}
data-col={chunkIndex * 2}
// @ts-ignore
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2}
ref={element => setCellRef(rowIndex, chunkIndex * 2, element)}
>
<ColumnLabelMargin
{...col}
headerText={col.columnName}
style={{ marginLeft: (col.uniquePath.length - 1) * 20 }}
extInfo={col.foreignKey ? ` -> ${col.foreignKey.refTableName}` : null}
/>
{col.foreignKey && (
<ShowFormButton
theme={theme}
className="buttonLike"
onClick={e => {
e.stopPropagation();
formDisplay.toggleExpandedColumn(col.uniqueName);
}}
>
<ExpandIcon isExpanded={formDisplay.isExpandedColumn(col.uniqueName)} />
</ShowFormButton>
)}
</TableHeaderCell>
<TableBodyCell
theme={theme}
data-row={rowIndex}
data-col={chunkIndex * 2 + 1}
// @ts-ignore
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1}
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
ref={element => setCellRef(rowIndex, chunkIndex * 2 + 1, element)}
>
{inplaceEditorState.cell &&
rowIndex == inplaceEditorState.cell[0] &&
chunkIndex * 2 + 1 == inplaceEditorState.cell[1] ? (
<InplaceEditor
widthPx={getCellWidth(rowIndex, chunkIndex * 2 + 1)}
inplaceEditorState={inplaceEditorState}
dispatchInsplaceEditor={dispatchInsplaceEditor}
cellValue={rowData[col.uniqueName]}
onSetValue={value => {
former.setCellValue(col.uniqueName, value);
}}
// grider={grider}
// rowIndex={rowIndex}
// uniqueName={col.uniqueName}
/>
) : (
<>
{rowData && (
<CellFormattedValue value={rowData[col.uniqueName]} dataType={col.dataType} theme={theme} />
)}
{!!col.hintColumnName &&
rowData &&
!(rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) && (
<HintSpan>{rowData[col.hintColumnName]}</HintSpan>
)}
{col.foreignKey && rowData && rowData[col.uniqueName] && (
<ShowFormButton
theme={theme}
className="buttonLike"
onClick={e => {
e.stopPropagation();
openReferenceForm(rowData, col, openNewTab, conid, database);
}}
>
<FontIcon icon="icon form" />
</ShowFormButton>
)}
</>
)}
</TableBodyCell>
</TableRow>
))}
</Table>
))}
<FocusField type="text" ref={focusFieldRef} onKeyDown={handleKeyDown} />
{toolbar}
</Wrapper>
{rowCountInfo && <RowCountLabel theme={theme}>{rowCountInfo}</RowCountLabel>}
</OuterWrapper>
);
}

View File

@@ -1,31 +0,0 @@
import React from 'react';
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
export default function FormViewContextMenu({ switchToTable, onNavigate, addToFilter, filterThisValue }) {
return (
<>
<DropDownMenuItem onClick={switchToTable} keyText="F4">
Table view
</DropDownMenuItem>
{addToFilter && <DropDownMenuItem onClick={addToFilter}>Add to filter</DropDownMenuItem>}
{filterThisValue && (
<DropDownMenuItem onClick={filterThisValue} keyText="Ctrl+F">
Filter this value
</DropDownMenuItem>
)}
<DropDownMenuDivider />
<DropDownMenuItem onClick={() => onNavigate('begin')} keyText="Ctrl+Home">
Navigate to begin
</DropDownMenuItem>
<DropDownMenuItem onClick={() => onNavigate('previous')} keyText="Ctrl+Up">
Navigate to previous
</DropDownMenuItem>
<DropDownMenuItem onClick={() => onNavigate('next')} keyText="Ctrl+Down">
Navigate to next
</DropDownMenuItem>
<DropDownMenuItem onClick={() => onNavigate('end')} keyText="Ctrl+End">
Navigate to end
</DropDownMenuItem>
</>
);
}

View File

@@ -1,116 +0,0 @@
import React from 'react';
import _ from 'lodash';
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
import styled from 'styled-components';
import ColumnLabel from '../datagrid/ColumnLabel';
import { TextField } from '../utility/inputs';
import { getFilterType } from 'dbgate-filterparser';
import DataFilterControl from '../datagrid/DataFilterControl';
import InlineButton from '../widgets/InlineButton';
import { FontIcon } from '../icons';
import keycodes from '../utility/keycodes';
const ColumnWrapper = styled.div`
margin: 5px;
`;
const ColumnNameWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
const TextFieldWrapper = styled.div`
display: flex;
`;
const StyledTextField = styled(TextField)`
flex: 1;
`;
function PrimaryKeyFilterEditor({ column, baseTable, formDisplay }) {
const value = formDisplay.getKeyValue(column.columnName);
const editorRef = React.useRef(null);
React.useEffect(() => {
if (editorRef.current) {
editorRef.current.value = value;
}
}, [value, editorRef.current]);
const applyFilter = () => {
formDisplay.requestKeyValue(column.columnName, editorRef.current.value);
};
const cancelFilter = () => {
formDisplay.cancelRequestKey();
formDisplay.reload();
};
const handleKeyDown = ev => {
if (ev.keyCode == keycodes.enter) {
applyFilter();
}
if (ev.keyCode == keycodes.escape) {
cancelFilter();
}
};
return (
<ColumnWrapper>
<ColumnNameWrapper>
<div>
<FontIcon icon="img primary-key" />
<ColumnLabel {...baseTable.columns.find(x => x.columnName == column.columnName)} />
</div>
{formDisplay.config.formViewKeyRequested && (
<InlineButton square onClick={cancelFilter}>
<FontIcon icon="icon delete" />
</InlineButton>
)}
</ColumnNameWrapper>
<TextFieldWrapper>
<StyledTextField editorRef={editorRef} onBlur={applyFilter} onKeyDown={handleKeyDown} />
</TextFieldWrapper>
</ColumnWrapper>
);
}
export default function FormViewFilters(props) {
const { formDisplay } = props;
if (!formDisplay || !formDisplay.baseTable || !formDisplay.baseTable.primaryKey) return null;
const { baseTable } = formDisplay;
const { formFilterColumns, filters } = formDisplay.config || {};
const allFilterNames = _.union(_.keys(filters || {}), formFilterColumns || []);
return (
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
{baseTable.primaryKey.columns.map(col => (
<PrimaryKeyFilterEditor key={col.columnName} baseTable={baseTable} column={col} formDisplay={formDisplay} />
))}
{allFilterNames.map(uniqueName => {
const column = formDisplay.columns.find(x => x.uniqueName == uniqueName)
// const column = baseTable.columns.find(x => x.columnName == columnName);
if (!column) return null;
return (
<ColumnWrapper key={uniqueName}>
<ColumnNameWrapper>
<ColumnLabel {...column} />
<InlineButton
square
onClick={() => {
formDisplay.removeFilter(column.uniqueName);
}}
>
<FontIcon icon="icon delete" />
</InlineButton>
</ColumnNameWrapper>
<DataFilterControl
filterType={getFilterType(column.dataType)}
filter={filters[column.uniqueName]}
setFilter={value => formDisplay.setFilter(column.uniqueName, value)}
/>
</ColumnWrapper>
);
})}
</ManagerInnerContainer>
);
}

View File

@@ -1,42 +0,0 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
export default function FormViewToolbar({ switchToTable, onNavigate, reload, reconnect, former, save }) {
return (
<>
<ToolbarButton onClick={switchToTable} icon="icon table">
Table view
</ToolbarButton>
<ToolbarButton onClick={() => onNavigate('begin')} icon="icon arrow-begin">
First
</ToolbarButton>
<ToolbarButton onClick={() => onNavigate('previous')} icon="icon arrow-left">
Previous
</ToolbarButton>
<ToolbarButton onClick={() => onNavigate('next')} icon="icon arrow-right">
Next
</ToolbarButton>
<ToolbarButton onClick={() => onNavigate('end')} icon="icon arrow-end">
Last
</ToolbarButton>
<ToolbarButton onClick={reload} icon="icon reload">
Refresh
</ToolbarButton>
<ToolbarButton onClick={reconnect} icon="icon connection">
Reconnect
</ToolbarButton>
<ToolbarButton disabled={!former.canUndo} onClick={() => former.undo()} icon="icon undo">
Undo
</ToolbarButton>
<ToolbarButton disabled={!former.canRedo} onClick={() => former.redo()} icon="icon redo">
Redo
</ToolbarButton>
<ToolbarButton disabled={!former.allowSave} onClick={save} icon="icon save">
Save
</ToolbarButton>
<ToolbarButton disabled={!former.containsChanges} onClick={() => former.revertAllChanges()} icon="icon close">
Revert
</ToolbarButton>
</>
);
}

View File

@@ -1,53 +0,0 @@
// export interface GriderRowStatus {
// status: 'regular' | 'updated' | 'deleted' | 'inserted';
// modifiedFields?: Set<string>;
// insertedFields?: Set<string>;
// deletedFields?: Set<string>;
// }
export default abstract class Former {
public rowData: any;
// getRowStatus(index): GriderRowStatus {
// const res: GriderRowStatus = {
// status: 'regular',
// };
// return res;
// }
beginUpdate() {}
endUpdate() {}
setCellValue(uniqueName: string, value: any) {}
revertRowChanges() {}
revertAllChanges() {}
undo() {}
redo() {}
get editable() {
return false;
}
get canInsert() {
return false;
}
get allowSave() {
return this.containsChanges;
}
get canUndo() {
return false;
}
get canRedo() {
return false;
}
get containsChanges() {
return false;
}
get disableLoadNextPage() {
return false;
}
get errors() {
return null;
}
updateRow(changeObject) {
for (const key of Object.keys(changeObject)) {
this.setCellValue(key, changeObject[key]);
}
}
}

View File

@@ -1,294 +0,0 @@
import { changeSetToSql, createChangeSet, TableFormViewDisplay } from 'dbgate-datalib';
import { findEngineDriver } from 'dbgate-tools';
import React from 'react';
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import useExtensions from '../utility/useExtensions';
import FormView from './FormView';
import axios from '../utility/axios';
import ChangeSetFormer from './ChangeSetFormer';
import ConfirmSqlModal from '../modals/ConfirmSqlModal';
import ErrorMessageModal from '../modals/ErrorMessageModal';
import { scriptToSql } from 'dbgate-sqltree';
import useModalState from '../modals/useModalState';
import useShowModal from '../modals/showModal';
import stableStringify from 'json-stable-stringify';
async function loadRow(props, sql) {
const { conid, database } = props;
if (!sql) return null;
const response = await axios.request({
url: 'database-connections/query-data',
method: 'post',
params: {
conid,
database,
},
data: { sql },
});
if (response.data.errorMessage) return response.data;
return response.data.rows[0];
}
export default function SqlFormView(props) {
// console.log('SqlFormView', props);
const {
formDisplay,
changeSetState,
dispatchChangeSet,
conid,
database,
onReferenceSourceChanged,
refReloadToken,
} = props;
// const [rowData, setRowData] = React.useState(null);
// const [reloadToken, setReloadToken] = React.useState(0);
// const [rowCountInfo, setRowCountInfo] = React.useState(null);
// const [isLoading, setIsLoading] = React.useState(false);
// const loadedFiltersRef = React.useRef('');
const confirmSqlModalState = useModalState();
const [confirmSql, setConfirmSql] = React.useState('');
const showModal = useShowModal();
const changeSet = changeSetState && changeSetState.value;
const changeSetRef = React.useRef(changeSet);
changeSetRef.current = changeSet;
const [loadProps, setLoadProps] = React.useState({
isLoadingData: false,
isLoadedData: false,
rowData: null,
isLoadingCount: false,
isLoadedCount: false,
loadedTime: new Date().getTime(),
allRowCount: null,
rowCountBefore: null,
errorMessage: null,
});
const {
isLoadingData,
rowData,
isLoadedData,
isLoadingCount,
isLoadedCount,
loadedTime,
allRowCount,
rowCountBefore,
errorMessage,
} = loadProps;
const handleLoadCurrentRow = async () => {
if (isLoadingData) return;
let newLoadedRow = false;
if (formDisplay.config.formViewKeyRequested || formDisplay.config.formViewKey) {
setLoadProps(oldLoadProps => ({
...oldLoadProps,
isLoadingData: true,
}));
const row = await loadRow(props, formDisplay.getCurrentRowQuery());
setLoadProps(oldLoadProps => ({
...oldLoadProps,
isLoadingData: false,
isLoadedData: true,
rowData: row,
loadedTime: new Date().getTime(),
}));
newLoadedRow = row;
}
if (formDisplay.config.formViewKeyRequested && newLoadedRow) {
formDisplay.cancelRequestKey(newLoadedRow);
}
if (!newLoadedRow && !formDisplay.config.formViewKeyRequested) {
await handleNavigate('first');
}
};
const handleLoadRowCount = async () => {
setLoadProps(oldLoadProps => ({
...oldLoadProps,
isLoadingCount: true,
}));
const countRow = await loadRow(props, formDisplay.getCountQuery());
const countBeforeRow = await loadRow(props, formDisplay.getBeforeCountQuery());
setLoadProps(oldLoadProps => ({
...oldLoadProps,
isLoadedCount: true,
isLoadingCount: false,
allRowCount: countRow ? parseInt(countRow.count) : null,
rowCountBefore: countBeforeRow ? parseInt(countBeforeRow.count) : null,
}));
};
const handleNavigate = async command => {
setLoadProps(oldLoadProps => ({
...oldLoadProps,
isLoadingData: true,
}));
const row = await loadRow(props, formDisplay.navigateRowQuery(command));
if (row) {
formDisplay.navigate(row);
}
setLoadProps(oldLoadProps => ({
...oldLoadProps,
isLoadingData: false,
isLoadedData: true,
isLoadedCount: false,
allRowCount: null,
rowCountBefore: null,
rowData: row,
loadedTime: new Date().getTime(),
}));
};
React.useEffect(() => {
if (onReferenceSourceChanged && rowData) onReferenceSourceChanged([rowData], loadedTime);
}, [onReferenceSourceChanged, rowData, refReloadToken]);
React.useEffect(() => {
if (!formDisplay.isLoadedCorrectly) return;
if (!isLoadedData && !isLoadingData) handleLoadCurrentRow();
if (isLoadedData && !isLoadingCount && !isLoadedCount) handleLoadRowCount();
});
// React.useEffect(() => {
// loadedFiltersRef.current = formDisplay ? stableStringify(formDisplay.config) : null;
// }, [rowData]);
// React.useEffect(() => {
// if (formDisplay) handleLoadCurrentRow();
// setRowCountInfo(null);
// handleLoadRowCount();
// }, [reloadToken]);
// React.useEffect(() => {
// if (!formDisplay.isLoadedCorrectly) return;
// if (
// formDisplay &&
// (!formDisplay.isLoadedCurrentRow(rowData) ||
// loadedFiltersRef.current != stableStringify(formDisplay.config.filters))
// ) {
// handleLoadCurrentRow();
// }
// setRowCountInfo(null);
// handleLoadRowCount();
// }, [formDisplay]);
const reload = () => {
setLoadProps({
isLoadingData: false,
isLoadedData: false,
isLoadingCount: false,
isLoadedCount: false,
rowData: null,
loadedTime: new Date().getTime(),
allRowCount: null,
rowCountBefore: null,
errorMessage: null,
});
};
React.useEffect(() => {
if (props.masterLoadedTime && props.masterLoadedTime > loadedTime) {
formDisplay.reload();
}
if (formDisplay.cache.refreshTime > loadedTime) {
reload();
}
});
const former = React.useMemo(() => new ChangeSetFormer(rowData, changeSetState, dispatchChangeSet, formDisplay), [
rowData,
changeSetState,
dispatchChangeSet,
formDisplay,
]);
function handleSave() {
const script = changeSetToSql(changeSetRef.current, formDisplay.dbinfo);
const sql = scriptToSql(formDisplay.driver, script);
setConfirmSql(sql);
confirmSqlModalState.open();
}
async function handleConfirmSql() {
const resp = await axios.request({
url: 'database-connections/query-data',
method: 'post',
params: {
conid,
database,
},
data: { sql: confirmSql },
});
const { errorMessage } = resp.data || {};
if (errorMessage) {
showModal(modalState => (
<ErrorMessageModal modalState={modalState} message={errorMessage} title="Error when saving" />
));
} else {
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
setConfirmSql(null);
formDisplay.reload();
// setReloadToken((x) => x + 1);
}
}
// const { config, setConfig, cache, setCache, schemaName, pureName, conid, database } = props;
// const { formViewKey } = config;
// const [display, setDisplay] = React.useState(null);
// const connection = useConnectionInfo({ conid });
// const dbinfo = useDatabaseInfo({ conid, database });
// const extensions = useExtensions();
// console.log('SqlFormView.props', props);
// React.useEffect(() => {
// const newDisplay = connection
// ? new TableFormViewDisplay(
// { schemaName, pureName },
// findEngineDriver(connection, extensions),
// config,
// setConfig,
// cache,
// setCache,
// dbinfo
// )
// : null;
// if (!newDisplay) return;
// if (display && display.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;
// setDisplay(newDisplay);
// }, [config, cache, conid, database, schemaName, pureName, dbinfo, extensions]);
return (
<>
<FormView
{...props}
rowData={rowData}
onNavigate={handleNavigate}
former={former}
onSave={handleSave}
isLoading={isLoadingData}
onReload={() => formDisplay.reload()}
onReconnect={async () => {
await axios.post('database-connections/refresh', { conid, database });
formDisplay.reload();
}}
allRowCount={allRowCount}
rowCountBefore={rowCountBefore}
/>
<ConfirmSqlModal
modalState={confirmSqlModalState}
sql={confirmSql}
engine={formDisplay.engine}
onConfirm={handleConfirmSql}
/>
</>
);
}

View File

@@ -1,30 +0,0 @@
import _ from 'lodash';
export default function openReferenceForm(rowData, column, openNewTab, conid, database) {
const formViewKey = _.fromPairs(
column.foreignKey.columns.map(({ refColumnName, columnName }) => [refColumnName, rowData[columnName]])
);
openNewTab(
{
title: column.foreignKey.refTableName,
icon: 'img table',
tabComponent: 'TableDataTab',
props: {
schemaName: column.foreignKey.refSchemaName,
pureName: column.foreignKey.refTableName,
conid,
database,
objectTypeField: 'tables',
},
},
{
grid: {
isFormView: true,
formViewKey,
},
},
{
forceNewTab: true,
}
);
}

View File

@@ -1,183 +0,0 @@
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import keycodes from '../utility/keycodes';
const Row = styled.div`
// margin-left: 5px;
// margin-right: 5px;
display: flex;
justify-content: space-between;
// padding: 5px;
cursor: pointer;
&:hover {
background-color: ${props => props.theme.manager_background_blue[1]};
}
`;
const Name = styled.div`
white-space: nowrap;
margin: 5px;
`;
const Buttons = styled.div`
white-space: nowrap;
`;
const Icon = styled(FontIcon)`
// margin-left: 5px;
position: relative;
top: 5px;
&:hover {
background-color: ${props => props.theme.manager_background3};
}
padding: 5px;
`;
const EditorInput = styled.input`
width: calc(100% - 10px);
background-color: ${props =>
// @ts-ignore
props.isError ? props.theme.manager_background_red[1] : props.theme.manager_background};
`;
function ColumnNameEditor({
onEnter,
onBlur = undefined,
focusOnCreate = false,
blurOnEnter = false,
existingNames,
defaultValue = '',
...other
}) {
const theme = useTheme();
const [value, setValue] = React.useState(defaultValue || '');
const editorRef = React.useRef(null);
const isError = value && existingNames && existingNames.includes(value);
const handleKeyDown = event => {
if (value && event.keyCode == keycodes.enter && !isError) {
onEnter(value);
setValue('');
if (blurOnEnter) editorRef.current.blur();
}
if (event.keyCode == keycodes.escape) {
setValue('');
editorRef.current.blur();
}
};
const handleBlur = () => {
if (value && !isError) {
onEnter(value);
setValue('');
}
if (onBlur) onBlur();
};
React.useEffect(() => {
if (focusOnCreate) editorRef.current.focus();
}, [focusOnCreate]);
return (
<EditorInput
theme={theme}
ref={editorRef}
type="text"
onKeyDown={handleKeyDown}
onBlur={handleBlur}
value={value}
onChange={ev => setValue(ev.target.value)}
// @ts-ignore
isError={isError}
{...other}
/>
);
}
function exchange(array, i1, i2) {
const i1r = (i1 + array.length) % array.length;
const i2r = (i2 + array.length) % array.length;
const res = [...array];
[res[i1r], res[i2r]] = [res[i2r], res[i1r]];
return res;
}
function ColumnManagerRow({ column, onEdit, onRemove, onUp, onDown }) {
const [isHover, setIsHover] = React.useState(false);
const theme = useTheme();
return (
<Row onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)} theme={theme}>
<Name>{column.columnName}</Name>
<Buttons>
<Icon icon="icon edit" onClick={onEdit} theme={theme} />
<Icon icon="icon delete" onClick={onRemove} theme={theme} />
<Icon icon="icon arrow-up" onClick={onUp} theme={theme} />
<Icon icon="icon arrow-down" onClick={onDown} theme={theme} />
</Buttons>
</Row>
);
}
function dispatchChangeColumns(props, func, rowFunc = null) {
const { modelState, dispatchModel } = props;
const model = modelState.value;
dispatchModel({
type: 'set',
value: {
rows: rowFunc ? model.rows.map(rowFunc) : model.rows,
structure: {
...model.structure,
columns: func(model.structure.columns),
},
},
});
}
export default function FreeTableColumnEditor(props) {
const { modelState, dispatchModel } = props;
const [editingColumn, setEditingColumn] = React.useState(null);
const model = modelState.value;
return (
<>
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
{model.structure.columns.map((column, index) =>
index == editingColumn ? (
<ColumnNameEditor
defaultValue={column.columnName}
onEnter={columnName => {
dispatchChangeColumns(
props,
cols => cols.map((col, i) => (index == i ? { columnName } : col)),
row => _.mapKeys(row, (v, k) => (k == column.columnName ? columnName : k))
);
}}
onBlur={() => setEditingColumn(null)}
focusOnCreate
blurOnEnter
existingNames={model.structure.columns.map(x => x.columnName)}
/>
) : (
<ColumnManagerRow
key={column.uniqueName}
column={column}
onEdit={() => setEditingColumn(index)}
onRemove={() => {
dispatchChangeColumns(props, cols => cols.filter((c, i) => i != index));
}}
onUp={() => {
dispatchChangeColumns(props, cols => exchange(cols, index, index - 1));
}}
onDown={() => {
dispatchChangeColumns(props, cols => exchange(cols, index, index + 1));
}}
/>
)
)}
<ColumnNameEditor
onEnter={columnName => {
dispatchChangeColumns(props, cols => [...cols, { columnName }]);
}}
placeholder="New column"
existingNames={model.structure.columns.map(x => x.columnName)}
/>
</ManagerInnerContainer>
</>
);
}

View File

@@ -1,92 +0,0 @@
import { runMacro } from 'dbgate-datalib';
import React from 'react';
import _ from 'lodash';
import styled from 'styled-components';
import { HorizontalSplitter, VerticalSplitter } from '../widgets/Splitter';
import FreeTableColumnEditor from './FreeTableColumnEditor';
import FreeTableGridCore from './FreeTableGridCore';
import MacroDetail from './MacroDetail';
import MacroManager from './MacroManager';
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
import useTheme from '../theme/useTheme';
const LeftContainer = styled.div`
background-color: ${props => props.theme.manager_background};
display: flex;
flex: 1;
`;
const DataGridContainer = styled.div`
position: relative;
flex-grow: 1;
`;
function extractMacroValuesForMacro(macroValues, macro) {
if (!macro) return {};
return {
..._.fromPairs((macro.args || []).filter(x => x.default != null).map(x => [x.name, x.default])),
..._.mapKeys(macroValues, (v, k) => k.replace(/^.*#/, '')),
};
}
export default function FreeTableGrid(props) {
const { modelState, dispatchModel } = props;
const theme = useTheme();
const [managerSize, setManagerSize] = React.useState(0);
const [selectedMacro, setSelectedMacro] = React.useState(null);
const [macroValues, setMacroValues] = React.useState({});
const [selectedCells, setSelectedCells] = React.useState([]);
const handleExecuteMacro = () => {
const newModel = runMacro(
selectedMacro,
extractMacroValuesForMacro(macroValues, selectedMacro),
modelState.value,
false,
selectedCells
);
dispatchModel({ type: 'set', value: newModel });
setSelectedMacro(null);
};
// console.log('macroValues', macroValues);
return (
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
<LeftContainer theme={theme}>
<WidgetColumnBar>
<WidgetColumnBarItem title="Columns" name="columns" height="40%">
<FreeTableColumnEditor {...props} />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Macros" name="macros">
<MacroManager
{...props}
managerSize={managerSize}
selectedMacro={selectedMacro}
setSelectedMacro={setSelectedMacro}
/>
</WidgetColumnBarItem>
</WidgetColumnBar>
</LeftContainer>
<DataGridContainer>
<VerticalSplitter initialValue="70%">
<FreeTableGridCore
{...props}
macroPreview={selectedMacro}
macroValues={extractMacroValuesForMacro(macroValues, selectedMacro)}
onSelectionChanged={setSelectedCells}
setSelectedMacro={setSelectedMacro}
/>
{!!selectedMacro && (
<MacroDetail
selectedMacro={selectedMacro}
setSelectedMacro={setSelectedMacro}
onChangeValues={setMacroValues}
macroValues={macroValues}
onExecute={handleExecuteMacro}
/>
)}
</VerticalSplitter>
</DataGridContainer>
</HorizontalSplitter>
);
}

View File

@@ -1,78 +0,0 @@
import { createGridCache, FreeTableGridDisplay } from 'dbgate-datalib';
import React from 'react';
import DataGridCore from '../datagrid/DataGridCore';
import useShowModal from '../modals/showModal';
import axios from '../utility/axios';
import keycodes from '../utility/keycodes';
import FreeTableGrider from './FreeTableGrider';
import MacroPreviewGrider from './MacroPreviewGrider';
import uuidv1 from 'uuid/v1';
import ImportExportModal from '../modals/ImportExportModal';
export default function FreeTableGridCore(props) {
const {
modelState,
dispatchModel,
config,
setConfig,
macroPreview,
macroValues,
onSelectionChanged,
setSelectedMacro,
} = props;
const [cache, setCache] = React.useState(createGridCache());
const [selectedCells, setSelectedCells] = React.useState([]);
const showModal = useShowModal();
const grider = React.useMemo(
() =>
macroPreview
? new MacroPreviewGrider(modelState.value, macroPreview, macroValues, selectedCells)
: FreeTableGrider.factory(props),
[
...FreeTableGrider.factoryDeps(props),
macroPreview,
macroPreview ? macroValues : null,
macroPreview ? selectedCells : null,
]
);
const display = React.useMemo(
() => new FreeTableGridDisplay(grider.model || modelState.value, config, setConfig, cache, setCache),
[modelState.value, config, cache, grider]
);
async function exportGrid() {
const jslid = uuidv1();
await axios.post('jsldata/save-free-table', { jslid, data: modelState.value });
const initialValues = {};
initialValues.sourceStorageType = 'jsldata';
initialValues.sourceJslId = jslid;
initialValues.sourceList = ['editor-data'];
showModal(modalState => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
}
const handleSelectionChanged = React.useCallback(
cells => {
if (onSelectionChanged) onSelectionChanged(cells);
setSelectedCells(cells);
},
[setSelectedCells]
);
const handleKeyDown = React.useCallback(event => {
if (event.keyCode == keycodes.escape) {
setSelectedMacro(null);
}
}, []);
return (
<DataGridCore
{...props}
grider={grider}
display={display}
onSelectionChanged={macroPreview ? handleSelectionChanged : null}
frameSelection={!!macroPreview}
exportGrid={exportGrid}
onKeyDown={handleKeyDown}
/>
);
}

View File

@@ -1,85 +0,0 @@
import { FreeTableModel } from 'dbgate-datalib';
import Grider, { GriderRowStatus } from '../datagrid/Grider';
export default class FreeTableGrider extends Grider {
public model: FreeTableModel;
private batchModel: FreeTableModel;
constructor(public modelState, public dispatchModel) {
super();
this.model = modelState && modelState.value;
}
getRowData(index: any) {
return this.model.rows[index];
}
get rowCount() {
return this.model.rows.length;
}
get currentModel(): FreeTableModel {
return this.batchModel || this.model;
}
set currentModel(value) {
if (this.batchModel) this.batchModel = value;
else this.dispatchModel({ type: 'set', value });
}
setCellValue(index: number, uniqueName: string, value: any) {
const model = this.currentModel;
if (model.rows[index])
this.currentModel = {
...model,
rows: model.rows.map((row, i) => (index == i ? { ...row, [uniqueName]: value } : row)),
};
}
get editable() {
return true;
}
get canInsert() {
return true;
}
get allowSave() {
return true;
}
insertRow(): number {
const model = this.currentModel;
this.currentModel = {
...model,
rows: [...model.rows, {}],
};
return this.currentModel.rows.length - 1;
}
deleteRow(index: number) {
const model = this.currentModel;
this.currentModel = {
...model,
rows: model.rows.filter((row, i) => index != i),
};
}
beginUpdate() {
this.batchModel = this.model;
}
endUpdate() {
if (this.model != this.batchModel) {
this.dispatchModel({ type: 'set', value: this.batchModel });
this.batchModel = null;
}
}
static factory({ modelState, dispatchModel }): FreeTableGrider {
return new FreeTableGrider(modelState, dispatchModel);
}
static factoryDeps({ modelState, dispatchModel }) {
return [modelState, dispatchModel];
}
undo() {
this.dispatchModel({ type: 'undo' });
}
redo() {
this.dispatchModel({ type: 'redo' });
}
get canUndo() {
return this.modelState.canUndo;
}
get canRedo() {
return this.modelState.canRedo;
}
}

View File

@@ -1,121 +0,0 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
import styled from 'styled-components';
import { TabPage, TabControl } from '../widgets/TabControl';
import dimensions from '../theme/dimensions';
import GenericEditor from '../sqleditor/GenericEditor';
import MacroParameters from './MacroParameters';
import { WidgetTitle } from '../widgets/WidgetStyles';
import { FormButton } from '../utility/forms';
import FormStyledButton from '../widgets/FormStyledButton';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
const Container = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
background: ${props => props.theme.gridheader_background_cyan[0]};
height: ${dimensions.toolBar.height}px;
min-height: ${dimensions.toolBar.height}px;
overflow: hidden;
border-top: 1px solid ${props => props.theme.border};
border-bottom: 1px solid ${props => props.theme.border};
`;
const Header = styled.div`
font-weight: bold;
margin-left: 10px;
display: flex;
`;
const HeaderText = styled.div`
margin-left: 10px;
`;
const MacroDetailContainer = styled.div`
position: absolute;
display: flex;
flex-direction: column;
top: 0;
left: 0;
right: 0;
bottom: 0;
`;
const MacroDetailTabWrapper = styled.div`
display: flex;
overflow-y: auto;
`;
const MacroSection = styled.div`
margin: 5px;
`;
const TextWrapper = styled.div`
margin: 5px;
`;
const Buttons = styled.div`
display: flex;
`;
function MacroHeader({ selectedMacro, setSelectedMacro, onExecute }) {
const theme = useTheme();
return (
<Container theme={theme}>
<Header>
<FontIcon icon="img macro" />
<HeaderText>{selectedMacro.title}</HeaderText>
</Header>
<Buttons>
<ToolbarButton icon="icon run" onClick={onExecute} patchY={6}>
Execute
</ToolbarButton>
<ToolbarButton icon="icon close" onClick={() => setSelectedMacro(null)} patchY={6}>
Close
</ToolbarButton>
</Buttons>
</Container>
);
}
export default function MacroDetail({ selectedMacro, setSelectedMacro, onChangeValues, macroValues, onExecute }) {
return (
<MacroDetailContainer>
<MacroHeader selectedMacro={selectedMacro} setSelectedMacro={setSelectedMacro} onExecute={onExecute} />
<TabControl>
<TabPage label="Macro detail" key="detail">
<MacroDetailTabWrapper>
<MacroSection>
<WidgetTitle>Execute</WidgetTitle>
<FormStyledButton value="Execute" onClick={onExecute} />
</MacroSection>
<MacroSection>
<WidgetTitle>Parameters</WidgetTitle>
{selectedMacro.args && selectedMacro.args.length > 0 ? (
<MacroParameters
key={selectedMacro.name}
args={selectedMacro.args}
onChangeValues={onChangeValues}
macroValues={macroValues}
namePrefix={`${selectedMacro.name}#`}
/>
) : (
<TextWrapper>This macro has no parameters</TextWrapper>
)}
</MacroSection>
<MacroSection>
<WidgetTitle>Description</WidgetTitle>
<TextWrapper>{selectedMacro.description}</TextWrapper>
</MacroSection>
</MacroDetailTabWrapper>
</TabPage>
<TabPage label="JavaScript" key="javascript">
<GenericEditor readOnly value={selectedMacro.code} mode="javascript" />
</TabPage>
</TabControl>
</MacroDetailContainer>
);
}

View File

@@ -1,41 +0,0 @@
import styled from 'styled-components';
import _ from 'lodash';
import React from 'react';
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
import SearchInput from '../widgets/SearchInput';
import { WidgetTitle } from '../widgets/WidgetStyles';
import macros from './macros';
import { AppObjectList } from '../appobj/AppObjectList';
import MacroAppObject from '../appobj/MacroAppObject';
const SearchBoxWrapper = styled.div`
display: flex;
margin-bottom: 5px;
`;
export default function MacroManager({ managerSize, selectedMacro, setSelectedMacro }) {
const [filter, setFilter] = React.useState('');
return (
<>
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
<SearchBoxWrapper>
<SearchInput placeholder="Search macros" filter={filter} setFilter={setFilter} />
</SearchBoxWrapper>
<AppObjectList
list={_.sortBy(macros, 'title')}
AppObjectComponent={MacroAppObject}
onObjectClick={macro => setSelectedMacro(macro)}
getCommonProps={data => ({
isBold: selectedMacro && selectedMacro.name == data.name,
})}
filter={filter}
groupFunc={data => data.group}
/>
{/* {macros.map((macro) => (
<MacroListItem key={`${macro.group}/${macro.name}`} macro={macro} />
))} */}
</ManagerInnerContainer>
</>
);
}

View File

@@ -1,17 +0,0 @@
import React from 'react';
import _ from 'lodash';
import FormArgumentList from '../utility/FormArgumentList';
import { FormProvider } from '../utility/FormProvider';
export default function MacroParameters({ args, onChangeValues, macroValues, namePrefix }) {
if (!args || args.length == 0) return null;
const initialValues = {
..._.fromPairs(args.filter(x => x.default != null).map(x => [`${namePrefix}${x.name}`, x.default])),
...macroValues,
};
return (
<FormProvider initialValues={initialValues}>
<FormArgumentList args={args} onChangeValues={onChangeValues} namePrefix={namePrefix} />
</FormProvider>
);
}

View File

@@ -1,40 +0,0 @@
import { FreeTableModel, MacroDefinition, MacroSelectedCell, runMacro } from 'dbgate-datalib';
import Grider, { GriderRowStatus } from '../datagrid/Grider';
import _ from 'lodash';
function convertToSet(row, field) {
if (!row) return null;
if (!row[field]) return null;
if (_.isSet(row[field])) return row[field];
return new Set(row[field]);
}
export default class MacroPreviewGrider extends Grider {
model: FreeTableModel;
_errors: string[] = [];
constructor(model: FreeTableModel, macro: MacroDefinition, macroArgs: {}, selectedCells: MacroSelectedCell[]) {
super();
this.model = runMacro(macro, macroArgs, model, true, selectedCells, this._errors);
}
get errors() {
return this._errors;
}
getRowStatus(index): GriderRowStatus {
const row = this.model.rows[index];
return {
status: (row && row.__rowStatus) || 'regular',
modifiedFields: convertToSet(row, '__modifiedFields'),
insertedFields: convertToSet(row, '__insertedFields'),
deletedFields: convertToSet(row, '__deletedFields'),
};
}
getRowData(index: any) {
return this.model.rows[index];
}
get rowCount() {
return this.model.rows.length;
}
}

View File

@@ -1,273 +0,0 @@
const macros = [
{
title: 'Remove diacritics',
name: 'removeDiacritics',
group: 'Text',
description: 'Removes diacritics from selected cells',
type: 'transformValue',
code: `return modules.lodash.deburr(value)`,
},
{
title: 'Search & replace text',
name: 'stringReplace',
group: 'Text',
description: 'Search & replace text or regular expression',
type: 'transformValue',
args: [
{
type: 'text',
label: 'Find',
name: 'find',
},
{
type: 'text',
label: 'Replace with',
name: 'replace',
},
{
type: 'checkbox',
label: 'Case sensitive',
name: 'caseSensitive',
},
{
type: 'checkbox',
label: 'Regular expression',
name: 'isRegex',
},
],
code: `
const rtext = args.isRegex ? args.find : modules.lodash.escapeRegExp(args.find);
const rflags = args.caseSensitive ? 'g' : 'ig';
return value ? value.toString().replace(new RegExp(rtext, rflags), args.replace || '') : value
`,
},
{
title: 'Change text case',
name: 'changeTextCase',
group: 'Text',
description: 'Uppercase, lowercase and other case functions',
type: 'transformValue',
args: [
{
type: 'select',
options: ['toUpper', 'toLower', 'lowerCase', 'upperCase', 'kebabCase', 'snakeCase', 'camelCase', 'startCase'],
label: 'Type',
name: 'type',
default: 'toUpper',
},
],
code: `return modules.lodash[args.type](value)`,
},
{
title: 'Row index',
name: 'rowIndex',
group: 'Tools',
description: 'Index of row from 1 (autoincrement)',
type: 'transformValue',
code: `return rowIndex + 1`,
},
{
title: 'Generate UUID',
name: 'uuidv1',
group: 'Tools',
description: 'Generate unique identifier',
type: 'transformValue',
args: [
{
type: 'select',
options: [
{ value: 'uuidv1', name: 'V1 - from timestamp' },
{ value: 'uuidv4', name: 'V4 - random generated' },
],
label: 'Version',
name: 'version',
default: 'uuidv1',
},
],
code: `return modules[args.version]()`,
},
{
title: 'Current date',
name: 'currentDate',
group: 'Tools',
description: 'Gets current date',
type: 'transformValue',
args: [
{
type: 'text',
label: 'Format',
name: 'format',
default: 'YYYY-MM-DD HH:mm:ss',
},
],
code: `return modules.moment().format(args.format)`,
},
{
title: 'Duplicate rows',
name: 'duplicateRows',
group: 'Tools',
description: 'Duplicate selected rows',
type: 'transformRows',
code: `
const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
const selectedRows = modules.lodash.groupBy(selectedCells, 'row');
const maxIndex = modules.lodash.max(selectedRowIndexes);
return [
...rows.slice(0, maxIndex + 1),
...selectedRowIndexes.map(index => ({
...modules.lodash.pick(rows[index], selectedRows[index].map(x => x.column)),
__rowStatus: 'inserted',
})),
...rows.slice(maxIndex + 1),
]
`,
},
{
title: 'Delete empty rows',
name: 'deleteEmptyRows',
group: 'Tools',
description: 'Delete empty rows - rows with all values null or empty string',
type: 'transformRows',
code: `
return rows.map(row => {
if (cols.find(col => row[col])) return row;
return {
...row,
__rowStatus: 'deleted',
};
})
`,
},
{
title: 'Duplicate columns',
name: 'duplicateColumns',
group: 'Tools',
description: 'Duplicate selected columns',
type: 'transformData',
code: `
const selectedColumnNames = modules.lodash.uniq(selectedCells.map(x => x.column));
const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
const addedColumnNames = selectedColumnNames.map(col => (args.prefix || '') + col + (args.postfix || ''));
const resultRows = rows.map((row, rowIndex) => ({
...row,
...(selectedRowIndexes.includes(rowIndex) ? modules.lodash.fromPairs(selectedColumnNames.map(col => [(args.prefix || '') + col + (args.postfix || ''), row[col]])) : {}),
__insertedFields: addedColumnNames,
}));
const resultCols = [
...cols,
...addedColumnNames,
];
return {
rows: resultRows,
cols: resultCols,
}
`,
args: [
{
type: 'text',
label: 'Prefix',
name: 'prefix',
},
{
type: 'text',
label: 'Postfix',
name: 'postfix',
default: '_copy',
},
],
},
{
title: 'Extract date fields',
name: 'extractDateFields',
group: 'Tools',
description: 'Extract yaear, month, day and other date/time fields from selection and adds it as new columns',
type: 'transformData',
code: `
const selectedColumnNames = modules.lodash.uniq(selectedCells.map(x => x.column));
const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
const addedColumnNames = modules.lodash.compact([args.year, args.month, args.day, args.hour, args.minute, args.second]);
const selectedRows = modules.lodash.groupBy(selectedCells, 'row');
const resultRows = rows.map((row, rowIndex) => {
if (!selectedRowIndexes.includes(rowIndex)) return {
...row,
__insertedFields: addedColumnNames,
};
let mom = null;
for(const cell of selectedRows[rowIndex]) {
const m = modules.moment(row[cell.column]);
if (m.isValid()) {
mom = m;
break;
}
}
if (!mom) return {
...row,
__insertedFields: addedColumnNames,
};
const fields = {
[args.year]: mom.year(),
[args.month]: mom.month() + 1,
[args.day]: mom.day(),
[args.hour]: mom.hour(),
[args.minute]: mom.minute(),
[args.second]: mom.second(),
};
return {
...row,
...modules.lodash.pick(fields, addedColumnNames),
__insertedFields: addedColumnNames,
}
});
const resultCols = [
...cols,
...addedColumnNames,
];
return {
rows: resultRows,
cols: resultCols,
}
`,
args: [
{
type: 'text',
label: 'Year name',
name: 'year',
default: 'year',
},
{
type: 'text',
label: 'Month name',
name: 'month',
default: 'month',
},
{
type: 'text',
label: 'Day name',
name: 'day',
default: 'day',
},
{
type: 'text',
label: 'Hour name',
name: 'hour',
default: 'hour',
},
{
type: 'text',
label: 'Minute name',
name: 'minute',
default: 'minute',
},
{
type: 'text',
label: 'Second name',
name: 'second',
default: 'second',
},
],
},
];
export default macros;

View File

@@ -1,14 +0,0 @@
import _ from 'lodash';
import useOpenNewTab from '../utility/useOpenNewTab';
export default function useNewFreeTable() {
const openNewTab = useOpenNewTab();
return ({ title = undefined, ...props } = {}) =>
openNewTab({
title: title || 'Data #',
icon: 'img free-table',
tabComponent: 'FreeTableTab',
props,
});
}

View File

@@ -1,123 +0,0 @@
import React from 'react';
const iconNames = {
'icon minus-box': 'mdi mdi-minus-box-outline',
'icon plus-box': 'mdi mdi-plus-box-outline',
'icon invisible-box': 'mdi mdi-minus-box-outline icon-invisible',
'icon cloud-upload': 'mdi mdi-cloud-upload',
'icon import': 'mdi mdi-application-import',
'icon export': 'mdi mdi-application-export',
'icon new-connection': 'mdi mdi-database-plus',
'icon tables': 'mdi mdi-table-multiple',
'icon favorite': 'mdi mdi-star',
'icon share': 'mdi mdi-share-variant',
'icon add': 'mdi mdi-plus-circle',
'icon connection': 'mdi mdi-connection',
'icon database': 'mdi mdi-database',
'icon server': 'mdi mdi-server',
'icon table': 'mdi mdi-table',
'icon archive': 'mdi mdi-archive',
'icon file': 'mdi mdi-file',
'icon loading': 'mdi mdi-loading mdi-spin',
'icon close': 'mdi mdi-close',
'icon filter': 'mdi mdi-filter',
'icon filter-off': 'mdi mdi-filter-off',
'icon reload': 'mdi mdi-reload',
'icon undo': 'mdi mdi-undo',
'icon redo': 'mdi mdi-redo',
'icon save': 'mdi mdi-content-save',
'icon account': 'mdi mdi-account',
'icon sql-file': 'mdi mdi-file',
'icon web': 'mdi mdi-web',
'icon home': 'mdi mdi-home',
'icon query-design': 'mdi mdi-vector-polyline-edit',
'icon form': 'mdi mdi-form-select',
'icon edit': 'mdi mdi-pencil',
'icon delete': 'mdi mdi-delete',
'icon arrow-up': 'mdi mdi-arrow-up',
'icon arrow-down': 'mdi mdi-arrow-down',
'icon arrow-left': 'mdi mdi-arrow-left',
'icon arrow-begin': 'mdi mdi-arrow-collapse-left',
'icon arrow-end': 'mdi mdi-arrow-collapse-right',
'icon arrow-right': 'mdi mdi-arrow-right',
'icon format-code': 'mdi mdi-code-tags-check',
'icon show-wizard': 'mdi mdi-comment-edit',
'icon disconnected': 'mdi mdi-lan-disconnect',
'icon theme': 'mdi mdi-brightness-6',
'icon error': 'mdi mdi-close-circle',
'icon ok': 'mdi mdi-check-circle',
'icon markdown': 'mdi mdi-application',
'icon preview': 'mdi mdi-file-find',
'icon eye': 'mdi mdi-eye',
'icon run': 'mdi mdi-play',
'icon chevron-down': 'mdi mdi-chevron-down',
'icon chevron-left': 'mdi mdi-chevron-left',
'icon chevron-right': 'mdi mdi-chevron-right',
'icon chevron-up': 'mdi mdi-chevron-up',
'icon plugin': 'mdi mdi-toy-brick',
'img ok': 'mdi mdi-check-circle color-green-8',
'img alert': 'mdi mdi-alert-circle color-blue-6',
'img error': 'mdi mdi-close-circle color-red-7',
'img warn': 'mdi mdi-alert color-gold-7',
// 'img statusbar-ok': 'mdi mdi-check-circle color-on-statusbar-green',
'img archive': 'mdi mdi-table color-gold-7',
'img archive-folder': 'mdi mdi-database-outline color-green-7',
'img autoincrement': 'mdi mdi-numeric-1-box-multiple-outline',
'img column': 'mdi mdi-table-column',
'img server': 'mdi mdi-server color-blue-7',
'img primary-key': 'mdi mdi-key-star color-yellow-7',
'img foreign-key': 'mdi mdi-key-link',
'img sql-file': 'mdi mdi-file',
'img shell': 'mdi mdi-flash color-blue-7',
'img chart': 'mdi mdi-chart-bar color-magenta-7',
'img markdown': 'mdi mdi-application color-red-7',
'img preview': 'mdi mdi-file-find color-red-7',
'img favorite': 'mdi mdi-star color-yellow-7',
'img query-design': 'mdi mdi-vector-polyline-edit color-red-7',
'img free-table': 'mdi mdi-table color-green-7',
'img macro': 'mdi mdi-hammer-wrench',
'img database': 'mdi mdi-database color-gold-7',
'img table': 'mdi mdi-table color-blue-7',
'img view': 'mdi mdi-table color-magenta-7',
'img procedure': 'mdi mdi-cog color-blue-7',
'img function': 'mdi mdi-function-variant',
'img sort-asc': 'mdi mdi-sort-alphabetical-ascending color-green',
'img sort-desc': 'mdi mdi-sort-alphabetical-descending color-green',
'img reference': 'mdi mdi-link-box',
'img link': 'mdi mdi-link',
'img filter': 'mdi mdi-filter',
'img group': 'mdi mdi-group',
};
export function FontIcon({ icon, className = '', ...other }) {
if (!icon) return null;
let cls = icon;
if (icon.startsWith('icon ') || icon.startsWith('img ')) {
cls = iconNames[icon];
if (!cls) return null;
}
return <span className={`${cls} ${className}`} {...other} />;
}
export function ExpandIcon({ isBlank = false, isExpanded = false, ...other }) {
if (isBlank) {
return <FontIcon icon="icon invisible-box" {...other} />;
}
return <FontIcon icon={isExpanded ? 'icon minus-box' : 'icon plus-box'} {...other} />;
}
export function ChevronExpandIcon({ isBlank = false, isExpanded = false, ...other }) {
if (isBlank) {
return <FontIcon icon="icon invisible-box" {...other} />;
}
return <FontIcon icon={isExpanded ? 'icon chevron-down' : 'icon chevron-right'} {...other} />;
}

View File

@@ -1,580 +0,0 @@
import React from 'react';
import _ from 'lodash';
import FormStyledButton from '../widgets/FormStyledButton';
import styled from 'styled-components';
import {
FormReactSelect,
FormConnectionSelect,
FormDatabaseSelect,
FormTablesSelect,
FormSchemaSelect,
FormArchiveFolderSelect,
FormArchiveFilesSelect,
} from '../utility/forms';
import { useArchiveFiles, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import TableControl, { TableColumn } from '../utility/TableControl';
import { TextField, SelectField, CheckboxField } from '../utility/inputs';
import { createPreviewReader, getActionOptions, getTargetName } from './createImpExpScript';
import getElectron from '../utility/getElectron';
import ErrorInfo from '../widgets/ErrorInfo';
import getAsArray from '../utility/getAsArray';
import LoadingInfo from '../widgets/LoadingInfo';
import SqlEditor from '../sqleditor/SqlEditor';
import { useUploadsProvider } from '../utility/UploadsProvider';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import { findFileFormat, getFileFormatDirections } from '../utility/fileformats';
import FormArgumentList from '../utility/FormArgumentList';
import useExtensions from '../utility/useExtensions';
import UploadButton from '../utility/UploadButton';
import useShowModal from '../modals/showModal';
import ChangeDownloadUrlModal from '../modals/ChangeDownloadUrlModal';
import { useForm } from '../utility/FormProvider';
const Container = styled.div`
// max-height: 50vh;
// overflow-y: scroll;
flex: 1;
`;
const Wrapper = styled.div`
display: flex;
`;
const SourceListWrapper = styled.div`
margin: 10px;
`;
const Column = styled.div`
margin: 10px;
flex: 1;
`;
const Label = styled.div`
margin: 5px;
margin-top: 15px;
color: ${props => props.theme.modal_font2};
`;
const SourceNameWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
const SourceNameButtons = styled.div`
display: flex;
`;
const IconButtonWrapper = styled.div`
&:hover {
background-color: ${props => props.theme.modal_background2};
}
cursor: pointer;
color: ${props => props.theme.modal_font_blue[7]};
margin-left: 5px;
`;
const SqlWrapper = styled.div`
position: relative;
height: 100px;
width: 20vw;
`;
const DragWrapper = styled.div`
padding: 10px;
background: ${props => props.theme.modal_background2};
`;
const ArrowWrapper = styled.div`
font-size: 30px;
color: ${props => props.theme.modal_font_blue[7]};
align-self: center;
`;
const Title = styled.div`
font-size: 20px;
text-align: center;
margin: 10px 0px;
`;
const ButtonsLine = styled.div`
display: flex;
`;
function getFileFilters(extensions, storageType) {
const res = [];
const format = findFileFormat(extensions, storageType);
if (format) res.push({ name: format.name, extensions: [format.extension] });
res.push({ name: 'All Files', extensions: ['*'] });
return res;
}
async function addFileToSourceListDefault({ fileName, shortName, isDownload }, newSources, newValues) {
const sourceName = shortName;
newSources.push(sourceName);
newValues[`sourceFile_${sourceName}`] = {
fileName,
isDownload,
};
}
async function addFilesToSourceList(extensions, files, values, setValues, preferedStorageType, setPreviewSource) {
const newSources = [];
const newValues = {};
const storage = preferedStorageType || values.sourceStorageType;
for (const file of getAsArray(files)) {
const format = findFileFormat(extensions, storage);
if (format) {
await (format.addFileToSourceList || addFileToSourceListDefault)(file, newSources, newValues);
}
}
newValues['sourceList'] = [...(values.sourceList || []).filter(x => !newSources.includes(x)), ...newSources];
if (preferedStorageType && preferedStorageType != values.sourceStorageType) {
newValues['sourceStorageType'] = preferedStorageType;
}
setValues({
...values,
...newValues,
});
if (setPreviewSource && newSources.length == 1) {
setPreviewSource(newSources[0]);
}
}
function ElectronFilesInput() {
const { values, setValues } = useForm();
const electron = getElectron();
const [isLoading, setIsLoading] = React.useState(false);
const extensions = useExtensions();
const handleClick = async () => {
const files = electron.remote.dialog.showOpenDialogSync(electron.remote.getCurrentWindow(), {
properties: ['openFile', 'multiSelections'],
filters: getFileFilters(extensions, values.sourceStorageType),
});
if (files) {
const path = window.require('path');
try {
setIsLoading(true);
await addFilesToSourceList(
extensions,
files.map(full => ({
fileName: full,
shortName: path.parse(full).name,
})),
values,
setValues
);
} finally {
setIsLoading(false);
}
}
};
return (
<>
<FormStyledButton type="button" value="Add file(s)" onClick={handleClick} />
{isLoading && <LoadingInfo message="Anaysing input files" />}
</>
);
}
function extractUrlName(url, values) {
const match = url.match(/\/([^/]+)($|\?)/);
if (match) {
const res = match[1];
if (res.includes('.')) {
return res.slice(0, res.indexOf('.'));
}
return res;
}
return `url${values && values.sourceList ? values.sourceList.length + 1 : '1'}`;
}
function FilesInput({ setPreviewSource = undefined }) {
const theme = useTheme();
const electron = getElectron();
const showModal = useShowModal();
const { values, setValues } = useForm();
const extensions = useExtensions();
const doAddUrl = url => {
addFilesToSourceList(
extensions,
[
{
fileName: url,
shortName: extractUrlName(url, values),
isDownload: true,
},
],
values,
setValues,
null,
setPreviewSource
);
};
const handleAddUrl = () =>
showModal(modalState => <ChangeDownloadUrlModal modalState={modalState} onConfirm={doAddUrl} />);
return (
<>
<ButtonsLine>
{electron ? <ElectronFilesInput /> : <UploadButton />}
<FormStyledButton value="Add web URL" onClick={handleAddUrl} />
</ButtonsLine>
<DragWrapper theme={theme}>Drag &amp; drop imported files here</DragWrapper>
</>
);
}
function SourceTargetConfig({
direction,
storageTypeField,
connectionIdField,
databaseNameField,
archiveFolderField,
schemaNameField,
tablesField = undefined,
engine = undefined,
setPreviewSource = undefined,
}) {
const extensions = useExtensions();
const theme = useTheme();
const { values, setFieldValue } = useForm();
const types =
values[storageTypeField] == 'jsldata'
? [{ value: 'jsldata', label: 'Query result data', directions: ['source'] }]
: [
{ value: 'database', label: 'Database', directions: ['source', 'target'] },
...extensions.fileFormats.map(format => ({
value: format.storageType,
label: `${format.name} files(s)`,
directions: getFileFormatDirections(format),
})),
{ value: 'query', label: 'SQL Query', directions: ['source'] },
{ value: 'archive', label: 'Archive', directions: ['source', 'target'] },
];
const storageType = values[storageTypeField];
const dbinfo = useDatabaseInfo({ conid: values[connectionIdField], database: values[databaseNameField] });
const archiveFiles = useArchiveFiles({ folder: values[archiveFolderField] });
const format = findFileFormat(extensions, storageType);
return (
<Column>
{direction == 'source' && (
<Title theme={theme}>
<FontIcon icon="icon import" /> Source configuration
</Title>
)}
{direction == 'target' && (
<Title theme={theme}>
<FontIcon icon="icon export" /> Target configuration
</Title>
)}
<FormReactSelect options={types.filter(x => x.directions.includes(direction))} name={storageTypeField} />
{(storageType == 'database' || storageType == 'query') && (
<>
<Label theme={theme}>Server</Label>
<FormConnectionSelect name={connectionIdField} />
<Label theme={theme}>Database</Label>
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} />
</>
)}
{storageType == 'database' && (
<>
<Label theme={theme}>Schema</Label>
<FormSchemaSelect conidName={connectionIdField} databaseName={databaseNameField} name={schemaNameField} />
{tablesField && (
<>
<Label theme={theme}>Tables/views</Label>
<FormTablesSelect
conidName={connectionIdField}
schemaName={schemaNameField}
databaseName={databaseNameField}
name={tablesField}
/>
<div>
<FormStyledButton
type="button"
value="All tables"
onClick={() =>
setFieldValue(
'sourceList',
_.uniq([...(values.sourceList || []), ...(dbinfo && dbinfo.tables.map(x => x.pureName))])
)
}
/>
<FormStyledButton
type="button"
value="All views"
onClick={() =>
setFieldValue(
'sourceList',
_.uniq([...(values.sourceList || []), ...(dbinfo && dbinfo.views.map(x => x.pureName))])
)
}
/>
<FormStyledButton type="button" value="Remove all" onClick={() => setFieldValue('sourceList', [])} />
</div>
</>
)}
</>
)}
{storageType == 'query' && (
<>
<Label theme={theme}>Query</Label>
<SqlWrapper>
<SqlEditor
value={values.sourceSql}
onChange={value => setFieldValue('sourceSql', value)}
engine={engine}
focusOnCreate
/>
</SqlWrapper>
</>
)}
{storageType == 'archive' && (
<>
<Label theme={theme}>Archive folder</Label>
<FormArchiveFolderSelect
name={archiveFolderField}
additionalFolders={_.compact([values[archiveFolderField]])}
/>
</>
)}
{storageType == 'archive' && direction == 'source' && (
<>
<Label theme={theme}>Source files</Label>
<FormArchiveFilesSelect folderName={values[archiveFolderField]} name={tablesField} />
<div>
<FormStyledButton
type="button"
value="All files"
onClick={() =>
setFieldValue(
'sourceList',
_.uniq([...(values.sourceList || []), ...(archiveFiles && archiveFiles.map(x => x.name))])
)
}
/>
<FormStyledButton type="button" value="Remove all" onClick={() => setFieldValue('sourceList', [])} />
</div>
</>
)}
{!!format && direction == 'source' && <FilesInput setPreviewSource={setPreviewSource} />}
{format && format.args && (
<FormArgumentList
args={format.args.filter(arg => !arg.direction || arg.direction == direction)}
namePrefix={`${direction}_${format.storageType}_`}
/>
)}
</Column>
);
}
function SourceName({ name }) {
const { values, setFieldValue } = useForm();
const theme = useTheme();
const showModal = useShowModal();
const obj = values[`sourceFile_${name}`];
const handleDelete = () => {
setFieldValue(
'sourceList',
values.sourceList.filter(x => x != name)
);
};
const doChangeUrl = url => {
setFieldValue(`sourceFile_${name}`, { fileName: url, isDownload: true });
};
const handleChangeUrl = () => {
showModal(modalState => (
<ChangeDownloadUrlModal modalState={modalState} url={obj.fileName} onConfirm={doChangeUrl} />
));
};
return (
<SourceNameWrapper>
<div>{name}</div>
<SourceNameButtons>
{obj && !!obj.isDownload && (
<IconButtonWrapper onClick={handleChangeUrl} theme={theme} title={obj && obj.fileName}>
<FontIcon icon="icon web" />
</IconButtonWrapper>
)}
<IconButtonWrapper onClick={handleDelete} theme={theme}>
<FontIcon icon="icon delete" />
</IconButtonWrapper>
</SourceNameButtons>
</SourceNameWrapper>
);
}
export default function ImportExportConfigurator({
uploadedFile = undefined,
openedFile = undefined,
onChangePreview = undefined,
}) {
const { values, setFieldValue, setValues } = useForm();
const targetDbinfo = useDatabaseInfo({ conid: values.targetConnectionId, database: values.targetDatabaseName });
const sourceConnectionInfo = useConnectionInfo({ conid: values.sourceConnectionId });
const { engine: sourceEngine } = sourceConnectionInfo || {};
const { sourceList } = values;
const { setUploadListener } = useUploadsProvider();
const theme = useTheme();
const [previewSource, setPreviewSource] = React.useState(null);
const extensions = useExtensions();
const handleUpload = React.useCallback(
file => {
addFilesToSourceList(
extensions,
[
{
fileName: file.filePath,
shortName: file.shortName,
},
],
values,
setValues,
!sourceList || sourceList.length == 0 ? file.storageType : null,
setPreviewSource
);
// setFieldValue('sourceList', [...(sourceList || []), file.originalName]);
},
[extensions, setFieldValue, sourceList, values]
);
React.useEffect(() => {
setUploadListener(() => handleUpload);
return () => {
setUploadListener(null);
};
}, [handleUpload]);
React.useEffect(() => {
if (uploadedFile) {
handleUpload(uploadedFile);
}
if (openedFile) {
addFilesToSourceList(
extensions,
[
{
fileName: openedFile.filePath,
shortName: openedFile.shortName,
},
],
values,
setValues,
!sourceList || sourceList.length == 0 ? openedFile.storageType : null,
setPreviewSource
);
}
}, []);
const supportsPreview =
!!findFileFormat(extensions, values.sourceStorageType) || values.sourceStorageType == 'archive';
const previewFileName =
previewSource && values[`sourceFile_${previewSource}`] && values[`sourceFile_${previewSource}`].fileName;
const handleChangePreviewSource = async () => {
if (previewSource && supportsPreview) {
const reader = await createPreviewReader(extensions, values, previewSource);
if (onChangePreview) onChangePreview(reader);
} else {
onChangePreview(null);
}
};
React.useEffect(() => {
handleChangePreviewSource();
}, [previewSource, supportsPreview, previewFileName]);
const oldValues = React.useRef({});
React.useEffect(() => {
const changed = _.pickBy(
values,
(v, k) => k.startsWith(`source_${values.sourceStorageType}_`) && oldValues.current[k] != v
);
if (!_.isEmpty(changed)) {
handleChangePreviewSource();
}
oldValues.current = values;
}, [values]);
return (
<Container>
<Wrapper>
<SourceTargetConfig
direction="source"
storageTypeField="sourceStorageType"
connectionIdField="sourceConnectionId"
databaseNameField="sourceDatabaseName"
archiveFolderField="sourceArchiveFolder"
schemaNameField="sourceSchemaName"
tablesField="sourceList"
engine={sourceEngine}
setPreviewSource={setPreviewSource}
/>
<ArrowWrapper theme={theme}>
<FontIcon icon="icon arrow-right" />
</ArrowWrapper>
<SourceTargetConfig
direction="target"
storageTypeField="targetStorageType"
connectionIdField="targetConnectionId"
databaseNameField="targetDatabaseName"
archiveFolderField="targetArchiveFolder"
schemaNameField="targetSchemaName"
/>
</Wrapper>
<SourceListWrapper>
<Title>
<FontIcon icon="icon tables" /> Map source tables/files
</Title>
<TableControl rows={sourceList || []}>
<TableColumn fieldName="source" header="Source" formatter={row => <SourceName name={row} />} />
<TableColumn
fieldName="action"
header="Action"
formatter={row => (
<SelectField
options={getActionOptions(extensions, row, values, targetDbinfo)}
value={values[`actionType_${row}`] || getActionOptions(extensions, row, values, targetDbinfo)[0].value}
onChange={e => setFieldValue(`actionType_${row}`, e.target.value)}
/>
)}
/>
<TableColumn
fieldName="target"
header="Target"
formatter={row => (
<TextField
value={getTargetName(extensions, row, values)}
onChange={e => setFieldValue(`targetName_${row}`, e.target.value)}
/>
)}
/>
<TableColumn
fieldName="preview"
header="Preview"
formatter={row =>
supportsPreview ? (
<CheckboxField
checked={previewSource == row}
onChange={e => {
if (e.target.checked) setPreviewSource(row);
else setPreviewSource(null);
}}
/>
) : null
}
/>
</TableControl>
{(sourceList || []).length == 0 && <ErrorInfo message="No source tables/files" icon="img alert" />}
</SourceListWrapper>
</Container>
);
}

View File

@@ -1,60 +0,0 @@
import { createGridCache, createGridConfig, FreeTableGridDisplay } from 'dbgate-datalib';
import React from 'react';
import DataGridCore from '../datagrid/DataGridCore';
import RowsArrayGrider from '../datagrid/RowsArrayGrider';
import axios from '../utility/axios';
import ErrorInfo from '../widgets/ErrorInfo';
import LoadingInfo from '../widgets/LoadingInfo';
export default function PreviewDataGrid({ reader, ...other }) {
const [isLoading, setIsLoading] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState(null);
const [model, setModel] = React.useState(null);
const [config, setConfig] = React.useState(createGridConfig());
const [cache, setCache] = React.useState(createGridCache());
const [grider, setGrider] = React.useState(null);
const handleLoadInitialData = async () => {
try {
if (!reader) {
setModel(null);
setGrider(null);
return;
}
setErrorMessage(null);
setIsLoading(true);
const resp = await axios.post('runners/load-reader', reader);
// @ts-ignore
setModel(resp.data);
setGrider(new RowsArrayGrider(resp.data.rows));
setIsLoading(false);
} catch (err) {
setIsLoading(false);
const errorMessage = (err && err.response && err.response.data && err.response.data.error) || 'Loading failed';
setErrorMessage(errorMessage);
console.error(err.response);
}
};
React.useEffect(() => {
handleLoadInitialData();
}, [reader]);
const display = React.useMemo(() => new FreeTableGridDisplay(model, config, setConfig, cache, setCache), [
model,
config,
cache,
grider,
]);
if (isLoading) {
return <LoadingInfo wrapper message="Loading data" />;
}
if (errorMessage) {
return <ErrorInfo message={errorMessage} />;
}
if (!grider) return null;
return <DataGridCore {...other} grider={grider} display={display} />;
}

View File

@@ -1,49 +0,0 @@
import _ from 'lodash';
import { extractShellApiFunctionName, extractShellApiPlugins } from 'dbgate-tools';
export default class ScriptWriter {
constructor(varCount = '0') {
this.s = '';
this.packageNames = [];
// this.engines = [];
this.varCount = parseInt(varCount) || 0;
}
allocVariable(prefix = 'var') {
this.varCount += 1;
return `${prefix}${this.varCount}`;
}
put(s = '') {
this.s += s;
this.s += '\n';
}
assign(variableName, functionName, props) {
this.put(`const ${variableName} = await ${extractShellApiFunctionName(functionName)}(${JSON.stringify(props)});`);
this.packageNames.push(...extractShellApiPlugins(functionName, props));
}
requirePackage(packageName) {
this.packageNames.push(packageName);
}
copyStream(sourceVar, targetVar) {
this.put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`);
}
comment(s) {
this.put(`// ${s}`);
}
getScript(schedule = null) {
const packageNames = this.packageNames;
let prefix = _.uniq(packageNames)
.map(packageName => `// @require ${packageName}\n`)
.join('');
if (schedule) prefix += `// @schedule ${schedule}`;
if (prefix) prefix += '\n';
return prefix + this.s;
}
}

View File

@@ -1,240 +0,0 @@
import _ from 'lodash';
import ScriptWriter from './ScriptWriter';
import getAsArray from '../utility/getAsArray';
import { getConnectionInfo } from '../utility/metadataLoaders';
import { findEngineDriver, findObjectLike } from 'dbgate-tools';
import { findFileFormat } from '../utility/fileformats';
export function getTargetName(extensions, source, values) {
const key = `targetName_${source}`;
if (values[key]) return values[key];
const format = findFileFormat(extensions, values.targetStorageType);
if (format) {
const res = format.getDefaultOutputName ? format.getDefaultOutputName(source, values) : null;
if (res) return res;
return `${source}.${format.extension}`;
}
return source;
}
function extractApiParameters(values, direction, format) {
const pairs = (format.args || [])
.filter(arg => arg.apiName)
.map(arg => [arg.apiName, values[`${direction}_${format.storageType}_${arg.name}`]])
.filter(x => x[1] != null);
return _.fromPairs(pairs);
}
async function getConnection(extensions, storageType, conid, database) {
if (storageType == 'database' || storageType == 'query') {
const conn = await getConnectionInfo({ conid });
const driver = findEngineDriver(conn, extensions);
return [
{
..._.omit(conn, ['_id', 'displayName']),
database,
},
driver,
];
}
return [null, null];
}
function getSourceExpr(extensions, sourceName, values, sourceConnection, sourceDriver) {
const { sourceStorageType } = values;
if (sourceStorageType == 'database') {
const fullName = { schemaName: values.sourceSchemaName, pureName: sourceName };
return [
'tableReader',
{
connection: sourceConnection,
...fullName,
},
];
}
if (sourceStorageType == 'query') {
return [
'queryReader',
{
connection: sourceConnection,
sql: values.sourceSql,
},
];
}
if (findFileFormat(extensions, sourceStorageType)) {
const sourceFile = values[`sourceFile_${sourceName}`];
const format = findFileFormat(extensions, sourceStorageType);
if (format && format.readerFunc) {
return [
format.readerFunc,
{
..._.omit(sourceFile, ['isDownload']),
...extractApiParameters(values, 'source', format),
},
];
}
}
if (sourceStorageType == 'jsldata') {
return ['jslDataReader', { jslid: values.sourceJslId }];
}
if (sourceStorageType == 'archive') {
return [
'archiveReader',
{
folderName: values.sourceArchiveFolder,
fileName: sourceName,
},
];
}
throw new Error(`Unknown source storage type: ${sourceStorageType}`);
}
function getFlagsFroAction(action) {
switch (action) {
case 'dropCreateTable':
return {
createIfNotExists: true,
dropIfExists: true,
};
case 'truncate':
return {
createIfNotExists: true,
truncate: true,
};
}
return {
createIfNotExists: true,
};
}
function getTargetExpr(extensions, sourceName, values, targetConnection, targetDriver) {
const { targetStorageType } = values;
const format = findFileFormat(extensions, targetStorageType);
if (format && format.writerFunc) {
const outputParams = format.getOutputParams && format.getOutputParams(sourceName, values);
return [
format.writerFunc,
{
...(outputParams
? outputParams
: {
fileName: getTargetName(extensions, sourceName, values),
}),
...extractApiParameters(values, 'target', format),
},
];
}
if (targetStorageType == 'database') {
return [
'tableWriter',
{
connection: targetConnection,
schemaName: values.targetSchemaName,
pureName: getTargetName(extensions, sourceName, values),
...getFlagsFroAction(values[`actionType_${sourceName}`]),
},
];
}
if (targetStorageType == 'archive') {
return [
'archiveWriter',
{
folderName: values.targetArchiveFolder,
fileName: getTargetName(extensions, sourceName, values),
},
];
}
throw new Error(`Unknown target storage type: ${targetStorageType}`);
}
export default async function createImpExpScript(extensions, values, addEditorInfo = true) {
const script = new ScriptWriter(values.startVariableIndex || 0);
const [sourceConnection, sourceDriver] = await getConnection(
extensions,
values.sourceStorageType,
values.sourceConnectionId,
values.sourceDatabaseName
);
const [targetConnection, targetDriver] = await getConnection(
extensions,
values.targetStorageType,
values.targetConnectionId,
values.targetDatabaseName
);
const sourceList = getAsArray(values.sourceList);
for (const sourceName of sourceList) {
const sourceVar = script.allocVariable();
// @ts-ignore
script.assign(sourceVar, ...getSourceExpr(extensions, sourceName, values, sourceConnection, sourceDriver));
const targetVar = script.allocVariable();
// @ts-ignore
script.assign(targetVar, ...getTargetExpr(extensions, sourceName, values, targetConnection, targetDriver));
script.copyStream(sourceVar, targetVar);
script.put();
}
if (addEditorInfo) {
script.comment('@ImportExportConfigurator');
script.comment(JSON.stringify(values));
}
return script.getScript(values.schedule);
}
export function getActionOptions(extensions, source, values, targetDbinfo) {
const res = [];
const targetName = getTargetName(extensions, source, values);
if (values.targetStorageType == 'database') {
let existing = findObjectLike(
{ schemaName: values.targetSchemaName, pureName: targetName },
targetDbinfo,
'tables'
);
if (existing) {
res.push({
label: 'Append data',
value: 'appendData',
});
res.push({
label: 'Truncate and import',
value: 'truncate',
});
res.push({
label: 'Drop and create table',
value: 'dropCreateTable',
});
} else {
res.push({
label: 'Create table',
value: 'createTable',
});
}
} else {
res.push({
label: 'Create file',
value: 'createFile',
});
}
return res;
}
export async function createPreviewReader(extensions, values, sourceName) {
const [sourceConnection, sourceDriver] = await getConnection(
extensions,
values.sourceStorageType,
values.sourceConnectionId,
values.sourceDatabaseName
);
const [functionName, props] = getSourceExpr(extensions, sourceName, values, sourceConnection, sourceDriver);
return {
functionName,
props: {
...props,
limitRows: 100,
},
};
}

View File

@@ -1,52 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, HelveticaNeue-Light, Ubuntu, Droid Sans,
sans-serif;
font-size: 14px;
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
*/
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
.RactModalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #00000080;
}
.icon-invisible {
visibility: hidden;
}
.largeFormMarker input[type='text'] {
width: 100%;
padding: 10px 10px;
font-size: 14px;
box-sizing: border-box;
border-radius: 4px;
}
.largeFormMarker input[type='password'] {
width: 100%;
padding: 10px 10px;
font-size: 14px;
box-sizing: border-box;
border-radius: 4px;
}
.largeFormMarker select {
width: 100%;
padding: 10px 10px;
font-size: 14px;
box-sizing: border-box;
border-radius: 4px;
}

View File

@@ -1,43 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import './index.css';
import '@mdi/font/css/materialdesignicons.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import 'ace-builds/src-noconflict/mode-sql';
import 'ace-builds/src-noconflict/mode-mysql';
import 'ace-builds/src-noconflict/mode-pgsql';
import 'ace-builds/src-noconflict/mode-sqlserver';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/mode-javascript';
import 'ace-builds/src-noconflict/mode-markdown';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-twilight';
import 'ace-builds/src-noconflict/ext-searchbox';
import 'ace-builds/src-noconflict/ext-language_tools';
import localStorageGarbageCollector from './utility/localStorageGarbageCollector';
// import 'ace-builds/src-noconflict/snippets/sqlserver';
// import 'ace-builds/src-noconflict/snippets/pgsql';
// import 'ace-builds/src-noconflict/snippets/mysql';
localStorageGarbageCollector();
window['dbgate_tabExports'] = {};
window['dbgate_getCurrentTabCommands'] = () => {
const tabid = window['dbgate_activeTabId'];
return _.mapValues(window['dbgate_tabExports'][tabid] || {}, v => !!v);
};
window['dbgate_tabCommand'] = cmd => {
const tabid = window['dbgate_activeTabId'];
const commands = window['dbgate_tabExports'][tabid];
const func = (commands || {})[cmd];
if (func) func();
};
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

View File

@@ -1,34 +0,0 @@
import React from 'react';
import Markdown from 'markdown-to-jsx';
import styled from 'styled-components';
import OpenChartLink from './OpenChartLink';
import MarkdownLink from './MarkdownLink';
import OpenSqlLink from './OpenSqlLink';
const Wrapper = styled.div`
padding: 10px;
overflow: auto;
flex: 1;
`;
export default function MarkdownExtendedView({ children }) {
return (
<Wrapper>
<Markdown
options={{
overrides: {
OpenChartLink: {
component: OpenChartLink,
},
OpenSqlLink: {
component: OpenSqlLink,
},
a: MarkdownLink,
},
}}
>
{children || ''}
</Markdown>
</Wrapper>
);
}

View File

@@ -1,13 +0,0 @@
import React from 'react';
import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
export default function MarkdownLink({ href, title, children }) {
const theme = useTheme();
return (
<StyledThemedLink theme={theme} href={href} title={title} target="_blank">
{children}
</StyledThemedLink>
);
}

View File

@@ -1,12 +0,0 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
export default function MarkdownToolbar({ showPreview }) {
return (
<>
<ToolbarButton onClick={showPreview} icon="icon preview">
Preview
</ToolbarButton>
</>
);
}

View File

@@ -1,35 +0,0 @@
import React from 'react';
import { useCurrentDatabase } from '../utility/globalState';
import axios from '../utility/axios';
import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
import useOpenNewTab from '../utility/useOpenNewTab';
export default function OpenChartLink({ file, children }) {
const openNewTab = useOpenNewTab();
const currentDb = useCurrentDatabase();
const theme = useTheme();
const handleClick = async () => {
const resp = await axios.post('files/load', { folder: 'charts', file, format: 'json' });
openNewTab(
{
title: file,
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid: currentDb && currentDb.connection && currentDb.connection._id,
database: currentDb && currentDb.name,
savedFile: file,
},
},
{ editor: resp.data }
);
};
return (
<StyledThemedLink theme={theme} onClick={handleClick}>
{children}
</StyledThemedLink>
);
}

View File

@@ -1,26 +0,0 @@
import React from 'react';
import axios from '../utility/axios';
import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
import useNewQuery from '../query/useNewQuery';
export default function OpenSqlLink({ file, children }) {
const newQuery = useNewQuery();
const theme = useTheme();
const handleClick = async () => {
const resp = await axios.post('files/load', { folder: 'sql', file, format: 'text' });
newQuery({
title: file,
initialData: resp.data,
// @ts-ignore
savedFile: file,
});
};
return (
<StyledThemedLink theme={theme} onClick={handleClick}>
{children}
</StyledThemedLink>
);
}

View File

@@ -1,93 +0,0 @@
import React from 'react';
import ModalBase from './ModalBase';
import ModalHeader from './ModalHeader';
import ModalContent from './ModalContent';
import ModalFooter from './ModalFooter';
import { useConfig } from '../utility/metadataLoaders';
import FormStyledButton from '../widgets/FormStyledButton';
import moment from 'moment';
import styled from 'styled-components';
import getElectron from '../utility/getElectron';
import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
const Container = styled.div`
display: flex;
`;
const TextContainer = styled.div``;
const StyledLine = styled.div`
margin: 5px;
`;
const StyledValue = styled.span`
font-weight: bold;
`;
function Line({ label, children }) {
return (
<StyledLine>
{label}: <StyledValue>{children}</StyledValue>
</StyledLine>
);
}
function Link({ label, children, href }) {
const electron = getElectron();
const theme = useTheme();
return (
<StyledLine>
{label}:{' '}
{electron ? (
<StyledThemedLink theme={theme} onClick={() => electron.shell.openExternal(href)}>
{children}
</StyledThemedLink>
) : (
<StyledThemedLink theme={theme} href={href} target="_blank" rel="noopener noreferrer">
{children}
</StyledThemedLink>
)}
</StyledLine>
);
}
export default function AboutModal({ modalState }) {
const config = useConfig();
const { version, buildTime } = config || {};
return (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>About DbGate</ModalHeader>
<ModalContent>
<Container>
<img
// eslint-disable-next-line
src={`${process.env.PUBLIC_URL}/logo192.png`}
/>
<TextContainer>
<Line label="Version">{version}</Line>
<Line label="Build date">{moment(buildTime).format('YYYY-MM-DD')}</Line>
<Link label="Web" href="https://dbgate.org">
dbgate.org
</Link>
<Link label="Source codes" href="https://github.com/dbgate/dbgate/">
github
</Link>
<Link label="Docker container" href="https://hub.docker.com/r/dbgate/dbgate">
docker hub
</Link>
<Link label="Online demo" href="https://demo.dbgate.org">
demo.dbgate.org
</Link>
<Link label="Search plugins" href="https://www.npmjs.com/search?q=keywords:dbgateplugin">
npmjs.com
</Link>
</TextContainer>
</Container>
</ModalContent>
<ModalFooter>
<FormStyledButton value="Close" onClick={() => modalState.close()} />
</ModalFooter>
</ModalBase>
);
}

View File

@@ -1,42 +0,0 @@
import React from 'react';
import ModalBase from './ModalBase';
import { FormButton, FormSubmit, FormTextField } from '../utility/forms';
import ModalHeader from './ModalHeader';
import ModalContent from './ModalContent';
import ModalFooter from './ModalFooter';
import FormStyledButton from '../widgets/FormStyledButton';
import { FormProvider } from '../utility/FormProvider';
export default function ChangeDownloadUrlModal({ modalState, url = '', onConfirm = undefined }) {
// const textFieldRef = React.useRef(null);
// React.useEffect(() => {
// if (textFieldRef.current) textFieldRef.current.focus();
// }, [textFieldRef.current]);
// const handleSubmit = () => async (values) => {
// onConfirm(values.url);
// modalState.close();
// };
const handleSubmit = React.useCallback(
async values => {
onConfirm(values.url);
modalState.close();
},
[modalState, onConfirm]
);
return (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>Download imported file from web</ModalHeader>
<FormProvider initialValues={{ url }}>
<ModalContent>
<FormTextField label="URL" name="url" style={{ width: '30vw' }} focused />
</ModalContent>
<ModalFooter>
<FormSubmit value="OK" onClick={handleSubmit} />
<FormStyledButton value="Cancel" onClick={() => modalState.close()} />
</ModalFooter>
</FormProvider>
</ModalBase>
);
}

View File

@@ -1,28 +0,0 @@
import React from 'react';
import ModalBase from './ModalBase';
import FormStyledButton from '../widgets/FormStyledButton';
import ModalFooter from './ModalFooter';
import ModalContent from './ModalContent';
import { FormSubmit } from '../utility/forms';
import { FormProvider } from '../utility/FormProvider';
export default function ConfirmModal({ message, modalState, onConfirm }) {
return (
<FormProvider>
<ModalBase modalState={modalState}>
<ModalContent>{message}</ModalContent>
<ModalFooter>
<FormSubmit
value="OK"
onClick={() => {
modalState.close();
onConfirm();
}}
/>
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
</ModalFooter>
</ModalBase>
</FormProvider>
);
}

View File

@@ -1,46 +0,0 @@
import React from 'react';
import ModalBase from './ModalBase';
import FormStyledButton from '../widgets/FormStyledButton';
import SqlEditor from '../sqleditor/SqlEditor';
import styled from 'styled-components';
import keycodes from '../utility/keycodes';
import ModalHeader from './ModalHeader';
import ModalContent from './ModalContent';
import ModalFooter from './ModalFooter';
const SqlWrapper = styled.div`
position: relative;
height: 30vh;
width: 40vw;
`;
export default function ConfirmSqlModal({ modalState, sql, engine, onConfirm }) {
const handleKeyDown = (data, hash, keyString, keyCode, event) => {
if (keyCode == keycodes.enter) {
event.preventDefault();
modalState.close();
onConfirm();
}
};
return (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>Save changes</ModalHeader>
<ModalContent>
<SqlWrapper>
<SqlEditor value={sql} engine={engine} focusOnCreate onKeyDown={handleKeyDown} readOnly />
</SqlWrapper>
</ModalContent>
<ModalFooter>
<FormStyledButton
value="OK"
onClick={() => {
modalState.close();
onConfirm();
}}
/>
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
</ModalFooter>
</ModalBase>
);
}

View File

@@ -1,349 +0,0 @@
import React from 'react';
import axios from '../utility/axios';
import ModalBase from './ModalBase';
import {
FormButton,
FormTextField,
FormSelectField,
FormSubmit,
FormPasswordField,
FormCheckboxField,
FormElectronFileSelector,
} from '../utility/forms';
import ModalHeader from './ModalHeader';
import ModalFooter from './ModalFooter';
import ModalContent from './ModalContent';
import useExtensions from '../utility/useExtensions';
import LoadingInfo from '../widgets/LoadingInfo';
import { FontIcon } from '../icons';
import { FormProvider, useForm } from '../utility/FormProvider';
import { TabControl, TabPage } from '../widgets/TabControl';
import { usePlatformInfo } from '../utility/metadataLoaders';
import getElectron from '../utility/getElectron';
import { FormFieldTemplateLarge, FormRowLarge } from '../utility/formStyle';
import styled from 'styled-components';
import { FlexCol3, FlexCol6, FlexCol9 } from '../utility/flexGrid';
// import FormikForm from '../utility/FormikForm';
const FlexContainer = styled.div`
display: flex;
`;
const TestResultContainer = styled.div`
margin-left: 10px;
align-self: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const ButtonsContainer = styled.div`
flex-shrink: 0;
`;
const AgentInfoWrap = styled.div`
margin-left: 20px;
margin-bottom: 20px;
`;
function DriverFields({ extensions }) {
const { values, setFieldValue } = useForm();
const { authType, engine } = values;
const driver = extensions.drivers.find(x => x.engine == engine);
// const { authTypes } = driver || {};
const [authTypes, setAuthTypes] = React.useState(null);
const currentAuthType = authTypes && authTypes.find(x => x.name == authType);
const loadAuthTypes = async () => {
const resp = await axios.post('plugins/auth-types', { engine });
setAuthTypes(resp.data);
if (resp.data && !currentAuthType) {
setFieldValue('authType', resp.data[0].name);
}
};
React.useEffect(() => {
setAuthTypes(null);
loadAuthTypes();
}, [values.engine]);
if (!driver) return null;
const disabledFields = (currentAuthType ? currentAuthType.disabledFields : null) || [];
return (
<>
{!!authTypes && (
<FormSelectField label="Authentication" name="authType">
{authTypes.map(auth => (
<option value={auth.name} key={auth.name}>
{auth.title}
</option>
))}
</FormSelectField>
)}
<FormRowLarge>
<FlexCol9
//@ts-ignore
marginRight={5}
>
<FormTextField
label="Server"
name="server"
disabled={disabledFields.includes('server')}
templateProps={{ noMargin: true }}
/>
</FlexCol9>
<FlexCol3>
<FormTextField
label="Port"
name="port"
disabled={disabledFields.includes('port')}
templateProps={{ noMargin: true }}
placeholder={driver && driver.defaultPort}
/>
</FlexCol3>
</FormRowLarge>
<FormRowLarge>
<FlexCol6
//@ts-ignore
marginRight={5}
>
<FormTextField
label="User"
name="user"
disabled={disabledFields.includes('user')}
templateProps={{ noMargin: true }}
/>
</FlexCol6>
<FlexCol6>
<FormPasswordField
label="Password"
name="password"
disabled={disabledFields.includes('password')}
templateProps={{ noMargin: true }}
/>
</FlexCol6>
</FormRowLarge>
{!disabledFields.includes('password') && (
<FormSelectField label="Password mode" name="passwordMode">
<option value="saveEncrypted">Save and encrypt</option>
<option value="saveRaw">Save raw (UNSAFE!!)</option>
</FormSelectField>
)}
</>
);
}
function SshTunnelFields() {
const { values, setFieldValue } = useForm();
const { useSshTunnel, sshMode, sshPort, sshKeyfile } = values;
const platformInfo = usePlatformInfo();
const electron = getElectron();
React.useEffect(() => {
if (useSshTunnel && !sshMode) {
setFieldValue('sshMode', 'userPassword');
}
if (useSshTunnel && !sshPort) {
setFieldValue('sshPort', '22');
}
if (useSshTunnel && sshMode == 'keyFile' && !sshKeyfile) {
setFieldValue('sshKeyfile', platformInfo.defaultKeyFile);
}
}, [useSshTunnel, sshMode]);
return (
<>
<FormCheckboxField label="Use SSH tunnel" name="useSshTunnel" />
<FormRowLarge>
<FlexCol9
//@ts-ignore
marginRight={5}
>
<FormTextField label="Host" name="sshHost" disabled={!useSshTunnel} templateProps={{ noMargin: true }} />
</FlexCol9>
<FlexCol3>
<FormTextField label="Port" name="sshPort" disabled={!useSshTunnel} templateProps={{ noMargin: true }} />
</FlexCol3>
</FormRowLarge>
<FormTextField label="Bastion host (Jump host)" name="sshBastionHost" disabled={!useSshTunnel} />
<FormSelectField label="SSH Authentication" name="sshMode" disabled={!useSshTunnel}>
<option value="userPassword">Username &amp; password</option>
<option value="agent">SSH agent</option>
{!!electron && <option value="keyFile">Key file</option>}
</FormSelectField>
{sshMode != 'userPassword' && <FormTextField label="Login" name="sshLogin" disabled={!useSshTunnel} />}
{sshMode == 'userPassword' && (
<FormRowLarge>
<FlexCol6
//@ts-ignore
marginRight={5}
>
<FormTextField label="Login" name="sshLogin" disabled={!useSshTunnel} templateProps={{ noMargin: true }} />
</FlexCol6>
<FlexCol6>
<FormPasswordField
label="Password"
name="sshPassword"
disabled={!useSshTunnel}
templateProps={{ noMargin: true }}
/>
</FlexCol6>
</FormRowLarge>
)}
{sshMode == 'keyFile' && (
<FormRowLarge>
<FlexCol6
//@ts-ignore
marginRight={5}
>
<FormElectronFileSelector
label="Private key file"
name="sshKeyfile"
disabled={!useSshTunnel}
templateProps={{ noMargin: true }}
/>
</FlexCol6>
<FlexCol6>
<FormPasswordField
label="Key file passphrase"
name="sshKeyfilePassword"
disabled={!useSshTunnel}
templateProps={{ noMargin: true }}
/>
</FlexCol6>
</FormRowLarge>
)}
{useSshTunnel && sshMode == 'agent' && (
<AgentInfoWrap>
{platformInfo.sshAuthSock ? (
<div>
<FontIcon icon="img ok" /> SSH Agent found
</div>
) : (
<div>
<FontIcon icon="img error" /> SSH Agent not found
</div>
)}
</AgentInfoWrap>
)}
</>
);
}
function SslFields() {
const { values } = useForm();
const { useSsl } = values;
const electron = getElectron();
return (
<>
<FormCheckboxField label="Use SSL" name="useSsl" />
<FormElectronFileSelector label="CA Cert (optional)" name="sslCaFile" disabled={!useSsl || !electron} />
<FormElectronFileSelector label="Certificate (optional)" name="sslCertFile" disabled={!useSsl || !electron} />
<FormElectronFileSelector label="Key file (optional)" name="sslKeyFile" disabled={!useSsl || !electron} />
<FormCheckboxField label="Reject unauthorized" name="sslRejectUnauthorized" disabled={!useSsl} />
</>
);
}
export default function ConnectionModal({ modalState, connection = undefined }) {
const [sqlConnectResult, setSqlConnectResult] = React.useState(null);
const extensions = useExtensions();
const [isTesting, setIsTesting] = React.useState(false);
const testIdRef = React.useRef(0);
const handleTest = async values => {
setIsTesting(true);
testIdRef.current += 1;
const testid = testIdRef.current;
const resp = await axios.post('connections/test', values);
if (testIdRef.current != testid) return;
setIsTesting(false);
setSqlConnectResult(resp.data);
};
const handleCancel = async () => {
testIdRef.current += 1; // invalidate current test
setIsTesting(false);
};
const handleSubmit = async values => {
axios.post('connections/save', values);
modalState.close();
};
return (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>{connection ? 'Edit connection' : 'Add connection'}</ModalHeader>
<FormProvider
initialValues={connection || { server: 'localhost', engine: 'mssql@dbgate-plugin-mssql' }}
template={FormFieldTemplateLarge}
>
<ModalContent noPadding>
<TabControl isInline>
<TabPage label="Main" key="main">
<FormSelectField label="Database engine" name="engine">
<option value="(select driver)"></option>
{extensions.drivers.map(driver => (
<option value={driver.engine} key={driver.engine}>
{driver.title}
</option>
))}
{/* <option value="mssql">Microsoft SQL Server</option>
<option value="mysql">MySQL</option>
<option value="postgres">Postgre SQL</option> */}
</FormSelectField>
<DriverFields extensions={extensions} />
<FormTextField label="Display name" name="displayName" />
</TabPage>
<TabPage label="SSH Tunnel" key="sshTunnel">
<SshTunnelFields />
</TabPage>
<TabPage label="SSL" key="ssl">
<SslFields />
</TabPage>
</TabControl>
</ModalContent>
<ModalFooter>
<FlexContainer>
<ButtonsContainer>
{isTesting ? (
<FormButton value="Cancel" onClick={handleCancel} />
) : (
<FormButton value="Test" onClick={handleTest} />
)}
<FormSubmit value="Save" onClick={handleSubmit} />
</ButtonsContainer>
<TestResultContainer>
{!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'connected' && (
<div>
Connected: <FontIcon icon="img ok" /> {sqlConnectResult.version}
</div>
)}
{!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'error' && (
<div>
Connect failed: <FontIcon icon="img error" /> {sqlConnectResult.error}
</div>
)}
{isTesting && (
<div>
<FontIcon icon="icon loading" /> Testing connection
</div>
)}
</TestResultContainer>
</FlexContainer>
</ModalFooter>
</FormProvider>
</ModalBase>
);
}

View File

@@ -1,30 +0,0 @@
import React from 'react';
import axios from '../utility/axios';
import ModalBase from './ModalBase';
import { FormButton, FormSubmit, FormTextField } from '../utility/forms';
import ModalHeader from './ModalHeader';
import ModalContent from './ModalContent';
import ModalFooter from './ModalFooter';
import { FormProvider } from '../utility/FormProvider';
export default function CreateDatabaseModal({ modalState, conid }) {
const handleSubmit = async values => {
const { name } = values;
axios.post('server-connections/create-database', { conid, name });
modalState.close();
};
return (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>Create database</ModalHeader>
<FormProvider initialValues={{ name: 'newdb' }}>
<ModalContent>
<FormTextField label="Database name" name="name" />
</ModalContent>
<ModalFooter>
<FormSubmit value="Create" onClick={handleSubmit} />
</ModalFooter>
</FormProvider>
</ModalBase>
);
}

Some files were not shown because too many files have changed in this diff Show More