mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-19 23:35:59 +00:00
remove web
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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" />;
|
||||
}
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
// }
|
||||
// }
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ManagerInnerContainer = styled.div`
|
||||
flex: 1 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
`;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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` }}> </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` }}> </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 }}>
|
||||
//
|
||||
// </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 }}>
|
||||
//
|
||||
// </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);
|
||||
// }
|
||||
// }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
@@ -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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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[];
|
||||
// };
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user