remove web

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

View File

@@ -1,28 +0,0 @@
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
"react/prop-types": "off",
"no-unused-vars": "warn"
}
};

View File

@@ -1,69 +0,0 @@
{
"name": "dbgate-web",
"version": "3.9.5",
"files": [
"build"
],
"scripts": {
"start": "cross-env BROWSER=none PORT=5000 react-scripts start",
"build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
"build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build",
"build": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
"prepublishOnly": "yarn build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"ts": "tsc"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react": "^16.9.17",
"@types/styled-components": "^4.4.2",
"dbgate-types": "^3.9.5",
"typescript": "^3.7.4",
"@ant-design/colors": "^5.0.0",
"@mdi/font": "^5.8.55",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"ace-builds": "^1.4.8",
"axios": "^0.19.0",
"chart.js": "^2.9.4",
"compare-versions": "^3.6.0",
"cross-env": "^6.0.3",
"dbgate-datalib": "^3.9.5",
"dbgate-sqltree": "^3.9.5",
"dbgate-tools": "^3.9.5",
"eslint": "^6.8.0",
"eslint-plugin-react": "^7.17.0",
"json-stable-stringify": "^1.0.1",
"localforage": "^1.9.0",
"markdown-to-jsx": "^7.1.0",
"randomcolor": "^0.6.2",
"react": "^16.12.0",
"react-ace": "^8.0.0",
"react-chartjs-2": "^2.11.1",
"react-dom": "^16.12.0",
"react-dropzone": "^11.2.3",
"react-helmet": "^6.1.0",
"react-json-view": "^1.19.1",
"react-modal": "^3.11.1",
"react-scripts": "3.3.0",
"react-select": "^3.1.0",
"resize-observer-polyfill": "^1.5.1",
"socket.io-client": "^2.3.0",
"sql-formatter": "^2.3.3",
"styled-components": "^4.4.1",
"uuid": "^3.4.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

View File

@@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description"
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>DbGate</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">Loading DbGate...</div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -1,25 +0,0 @@
{
"short_name": "DbGate",
"name": "DbGate database tool",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -1,2 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

View File

@@ -1,16 +0,0 @@
body {
background: #666;
}
div {
color: white;
font-size: 25pt;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
text-align: center;
font-family: Arial, Helvetica, sans-serif;
margin-top: 40px;
}

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="splash.css">
</head>
<body>
<div>Starting DbGate...</div>
</body>
</html>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="116" height="116" id="svg2">
<defs id="defs4"/>
<metadata id="metadata7">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<text x="10.710938" y="111.5" id="text2996" xml:space="preserve" style="font-size:144px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"><tspan x="10.710938" y="111.5" id="tspan2998" style="font-size:150px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold">?</tspan></text>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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