mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-21 03:36:01 +00:00
remove web
This commit is contained in:
@@ -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"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 |
@@ -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 |
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="splash.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div>Starting DbGate...</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -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 |
@@ -1,38 +0,0 @@
|
|||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-logo {
|
|
||||||
height: 40vmin;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.App-logo {
|
|
||||||
animation: App-logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
background-color: #282c34;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: #61dafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import './index.css';
|
|
||||||
import Screen from './Screen';
|
|
||||||
import {
|
|
||||||
CurrentWidgetProvider,
|
|
||||||
CurrentDatabaseProvider,
|
|
||||||
OpenedTabsProvider,
|
|
||||||
OpenedConnectionsProvider,
|
|
||||||
LeftPanelWidthProvider,
|
|
||||||
CurrentArchiveProvider,
|
|
||||||
CurrentThemeProvider,
|
|
||||||
} from './utility/globalState';
|
|
||||||
import { SocketProvider } from './utility/SocketProvider';
|
|
||||||
import ConnectionsPinger from './utility/ConnectionsPinger';
|
|
||||||
import { ModalLayerProvider } from './modals/showModal';
|
|
||||||
import UploadsProvider from './utility/UploadsProvider';
|
|
||||||
import ThemeHelmet from './themes/ThemeHelmet';
|
|
||||||
import PluginsProvider from './plugins/PluginsProvider';
|
|
||||||
import { ExtensionsProvider } from './utility/useExtensions';
|
|
||||||
import { MenuLayerProvider } from './modals/showMenu';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<CurrentWidgetProvider>
|
|
||||||
<CurrentDatabaseProvider>
|
|
||||||
<SocketProvider>
|
|
||||||
<OpenedTabsProvider>
|
|
||||||
<OpenedConnectionsProvider>
|
|
||||||
<LeftPanelWidthProvider>
|
|
||||||
<ConnectionsPinger>
|
|
||||||
<PluginsProvider>
|
|
||||||
<ExtensionsProvider>
|
|
||||||
<CurrentArchiveProvider>
|
|
||||||
<CurrentThemeProvider>
|
|
||||||
<UploadsProvider>
|
|
||||||
<ModalLayerProvider>
|
|
||||||
<MenuLayerProvider>
|
|
||||||
<ThemeHelmet />
|
|
||||||
<Screen />
|
|
||||||
</MenuLayerProvider>
|
|
||||||
</ModalLayerProvider>
|
|
||||||
</UploadsProvider>
|
|
||||||
</CurrentThemeProvider>
|
|
||||||
</CurrentArchiveProvider>
|
|
||||||
</ExtensionsProvider>
|
|
||||||
</PluginsProvider>
|
|
||||||
</ConnectionsPinger>
|
|
||||||
</LeftPanelWidthProvider>
|
|
||||||
</OpenedConnectionsProvider>
|
|
||||||
</OpenedTabsProvider>
|
|
||||||
</SocketProvider>
|
|
||||||
</CurrentDatabaseProvider>
|
|
||||||
</CurrentWidgetProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
const { getByText } = render(<App />);
|
|
||||||
const linkElement = getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { FontIcon } from './icons';
|
|
||||||
import useTheme from './theme/useTheme';
|
|
||||||
import getElectron from './utility/getElectron';
|
|
||||||
import useExtensions from './utility/useExtensions';
|
|
||||||
|
|
||||||
const TargetStyled = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
display: flex;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: ${props => props.theme.main_background_blue[3]};
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
z-index: 1000;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InfoBox = styled.div``;
|
|
||||||
|
|
||||||
const IconWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
font-size: 50px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InfoWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
margin-top: 10px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TitleWrapper = styled.div`
|
|
||||||
font-size: 30px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { fileFormats } = useExtensions();
|
|
||||||
const electron = getElectron();
|
|
||||||
const fileTypeNames = fileFormats.filter(x => x.readerFunc).map(x => x.name);
|
|
||||||
if (electron) fileTypeNames.push('SQL');
|
|
||||||
return (
|
|
||||||
!!isDragActive && (
|
|
||||||
<TargetStyled theme={theme}>
|
|
||||||
<InfoBox>
|
|
||||||
<IconWrapper>
|
|
||||||
<FontIcon icon="icon cloud-upload" />
|
|
||||||
</IconWrapper>
|
|
||||||
<TitleWrapper>Drop the files to upload to DbGate</TitleWrapper>
|
|
||||||
<InfoWrapper>Supported file types: {fileTypeNames.join(', ')}</InfoWrapper>
|
|
||||||
</InfoBox>
|
|
||||||
<input {...inputProps} />
|
|
||||||
</TargetStyled>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import dimensions from './theme/dimensions';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import TabsPanel from './TabsPanel';
|
|
||||||
import TabContent from './TabContent';
|
|
||||||
import WidgetIconPanel from './widgets/WidgetIconPanel';
|
|
||||||
import { useCurrentWidget, useLeftPanelWidth, useSetLeftPanelWidth } from './utility/globalState';
|
|
||||||
import WidgetContainer from './widgets/WidgetContainer';
|
|
||||||
import ToolBar from './widgets/Toolbar';
|
|
||||||
import StatusBar from './widgets/StatusBar';
|
|
||||||
import { useSplitterDrag, HorizontalSplitHandle } from './widgets/Splitter';
|
|
||||||
import { ModalLayer } from './modals/showModal';
|
|
||||||
import DragAndDropFileTarget from './DragAndDropFileTarget';
|
|
||||||
import { useUploadsZone } from './utility/UploadsProvider';
|
|
||||||
import useTheme from './theme/useTheme';
|
|
||||||
import { MenuLayer } from './modals/showMenu';
|
|
||||||
import ErrorBoundary, { ErrorBoundaryTest } from './utility/ErrorBoundary';
|
|
||||||
|
|
||||||
const BodyDiv = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
top: ${dimensions.tabsPanel.height + dimensions.toolBar.height}px;
|
|
||||||
left: ${props => props.contentLeft}px;
|
|
||||||
bottom: ${dimensions.statusBar.height}px;
|
|
||||||
right: 0;
|
|
||||||
background-color: ${props => props.theme.content_background};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ToolBarDiv = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: ${props => props.theme.toolbar_background};
|
|
||||||
height: ${dimensions.toolBar.height}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const IconBar = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
top: ${dimensions.toolBar.height}px;
|
|
||||||
left: 0;
|
|
||||||
bottom: ${dimensions.statusBar.height}px;
|
|
||||||
width: ${dimensions.widgetMenu.iconSize}px;
|
|
||||||
background-color: ${props => props.theme.widget_background};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LeftPanel = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
top: ${dimensions.toolBar.height}px;
|
|
||||||
left: ${dimensions.widgetMenu.iconSize}px;
|
|
||||||
bottom: ${dimensions.statusBar.height}px;
|
|
||||||
background-color: ${props => props.theme.left_background};
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabsPanelContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
position: fixed;
|
|
||||||
top: ${dimensions.toolBar.height}px;
|
|
||||||
left: ${props => props.contentLeft}px;
|
|
||||||
height: ${dimensions.tabsPanel.height}px;
|
|
||||||
right: 0;
|
|
||||||
background-color: ${props => props.theme.tabs_background2};
|
|
||||||
border-top: 1px solid ${props => props.theme.border};
|
|
||||||
|
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
height: 7px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StausBarContainer = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
height: ${dimensions.statusBar.height}px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: ${props => props.theme.statusbar_background};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ScreenHorizontalSplitHandle = styled(HorizontalSplitHandle)`
|
|
||||||
position: absolute;
|
|
||||||
top: ${dimensions.toolBar.height}px;
|
|
||||||
bottom: ${dimensions.statusBar.height}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// const StyledRoot = styled.div`
|
|
||||||
// // color: ${(props) => props.theme.fontColor};
|
|
||||||
// `;
|
|
||||||
|
|
||||||
export default function Screen() {
|
|
||||||
const theme = useTheme();
|
|
||||||
const currentWidget = useCurrentWidget();
|
|
||||||
const leftPanelWidth = useLeftPanelWidth();
|
|
||||||
const setLeftPanelWidth = useSetLeftPanelWidth();
|
|
||||||
const contentLeft = currentWidget
|
|
||||||
? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness
|
|
||||||
: dimensions.widgetMenu.iconSize;
|
|
||||||
const toolbarPortalRef = React.useRef();
|
|
||||||
const statusbarPortalRef = React.useRef();
|
|
||||||
const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff));
|
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useUploadsZone();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...getRootProps()}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
<ToolBarDiv theme={theme}>
|
|
||||||
<ToolBar toolbarPortalRef={toolbarPortalRef} />
|
|
||||||
</ToolBarDiv>
|
|
||||||
<IconBar theme={theme}>
|
|
||||||
<WidgetIconPanel />
|
|
||||||
</IconBar>
|
|
||||||
{!!currentWidget && (
|
|
||||||
<LeftPanel theme={theme}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
<WidgetContainer />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</LeftPanel>
|
|
||||||
)}
|
|
||||||
{!!currentWidget && (
|
|
||||||
<ScreenHorizontalSplitHandle
|
|
||||||
onMouseDown={onSplitDown}
|
|
||||||
theme={theme}
|
|
||||||
style={{ left: leftPanelWidth + dimensions.widgetMenu.iconSize }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TabsPanelContainer contentLeft={contentLeft} theme={theme}>
|
|
||||||
<TabsPanel></TabsPanel>
|
|
||||||
</TabsPanelContainer>
|
|
||||||
<BodyDiv contentLeft={contentLeft} theme={theme}>
|
|
||||||
<TabContent toolbarPortalRef={toolbarPortalRef} statusbarPortalRef={statusbarPortalRef} />
|
|
||||||
</BodyDiv>
|
|
||||||
<StausBarContainer theme={theme}>
|
|
||||||
<StatusBar statusbarPortalRef={statusbarPortalRef} />
|
|
||||||
</StausBarContainer>
|
|
||||||
<ModalLayer />
|
|
||||||
<MenuLayer />
|
|
||||||
|
|
||||||
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import tabs from './tabs';
|
|
||||||
import { useOpenedTabs } from './utility/globalState';
|
|
||||||
import ErrorBoundary from './utility/ErrorBoundary';
|
|
||||||
|
|
||||||
const TabContainerStyled = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
visibility: ${props =>
|
|
||||||
// @ts-ignore
|
|
||||||
props.tabVisible ? 'visible' : 'hidden'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
function TabContainer({ TabComponent, ...props }) {
|
|
||||||
const { tabVisible, tabid, toolbarPortalRef, statusbarPortalRef } = props;
|
|
||||||
return (
|
|
||||||
// @ts-ignore
|
|
||||||
<TabContainerStyled tabVisible={tabVisible}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
<TabComponent
|
|
||||||
{...props}
|
|
||||||
tabid={tabid}
|
|
||||||
tabVisible={tabVisible}
|
|
||||||
toolbarPortalRef={toolbarPortalRef}
|
|
||||||
statusbarPortalRef={statusbarPortalRef}
|
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</TabContainerStyled>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TabContainerMemo = React.memo(TabContainer);
|
|
||||||
|
|
||||||
function createTabComponent(selectedTab) {
|
|
||||||
const TabComponent = tabs[selectedTab.tabComponent];
|
|
||||||
if (TabComponent) {
|
|
||||||
return {
|
|
||||||
TabComponent,
|
|
||||||
props: selectedTab.props,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TabContent({ toolbarPortalRef, statusbarPortalRef }) {
|
|
||||||
const files = useOpenedTabs();
|
|
||||||
|
|
||||||
const [mountedTabs, setMountedTabs] = React.useState({});
|
|
||||||
|
|
||||||
const selectedTab = files.find(x => x.selected && x.closedTime == null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// cleanup closed tabs
|
|
||||||
|
|
||||||
if (
|
|
||||||
_.difference(
|
|
||||||
_.keys(mountedTabs),
|
|
||||||
_.map(
|
|
||||||
files.filter(x => x.closedTime == null),
|
|
||||||
'tabid'
|
|
||||||
)
|
|
||||||
).length > 0
|
|
||||||
) {
|
|
||||||
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find(x => x.tabid == k && x.closedTime == null)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTab) {
|
|
||||||
const { tabid } = selectedTab;
|
|
||||||
if (tabid && !mountedTabs[tabid])
|
|
||||||
setMountedTabs({
|
|
||||||
...mountedTabs,
|
|
||||||
[tabid]: createTabComponent(selectedTab),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [mountedTabs, files]);
|
|
||||||
|
|
||||||
return _.keys(mountedTabs).map(tabid => {
|
|
||||||
const { TabComponent, props } = mountedTabs[tabid];
|
|
||||||
const tabVisible = tabid == (selectedTab && selectedTab.tabid);
|
|
||||||
return (
|
|
||||||
<TabContainerMemo
|
|
||||||
key={tabid}
|
|
||||||
{...props}
|
|
||||||
tabid={tabid}
|
|
||||||
tabVisible={tabVisible}
|
|
||||||
toolbarPortalRef={toolbarPortalRef}
|
|
||||||
statusbarPortalRef={statusbarPortalRef}
|
|
||||||
TabComponent={TabComponent}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { DropDownMenuItem, DropDownMenuDivider } from './modals/DropDownMenu';
|
|
||||||
|
|
||||||
import { useOpenedTabs, useSetOpenedTabs, useCurrentDatabase, useSetCurrentDatabase } from './utility/globalState';
|
|
||||||
import { getConnectionInfo } from './utility/metadataLoaders';
|
|
||||||
import { FontIcon } from './icons';
|
|
||||||
import useTheme from './theme/useTheme';
|
|
||||||
import usePropsCompare from './utility/usePropsCompare';
|
|
||||||
import { useShowMenu } from './modals/showMenu';
|
|
||||||
import { setSelectedTabFunc } from './utility/common';
|
|
||||||
import getElectron from './utility/getElectron';
|
|
||||||
|
|
||||||
// const files = [
|
|
||||||
// { name: 'app.js' },
|
|
||||||
// { name: 'BranchCategory', type: 'table', selected: true },
|
|
||||||
// { name: 'ApplicationList' },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
const DbGroupHandler = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
align-content: stretch;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DbWrapperHandler = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DbNameWrapper = styled.div`
|
|
||||||
text-align: center;
|
|
||||||
font-size: 8pt;
|
|
||||||
border-bottom: 1px solid ${props => props.theme.border};
|
|
||||||
border-right: 1px solid ${props => props.theme.border};
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
padding: 1px;
|
|
||||||
position: relative;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
// height: 15px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: ${props => props.theme.tabs_background3};
|
|
||||||
}
|
|
||||||
background-color: ${props =>
|
|
||||||
// @ts-ignore
|
|
||||||
props.selected ? props.theme.tabs_background1 : 'inherit'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
// const DbNameWrapperInner = styled.div`
|
|
||||||
// position: absolute;
|
|
||||||
// white-space: nowrap;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
const FileTabItem = styled.div`
|
|
||||||
border-right: 1px solid ${props => props.theme.border};
|
|
||||||
padding-left: 15px;
|
|
||||||
padding-right: 15px;
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-grow: 1;
|
|
||||||
min-width: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
&:hover {
|
|
||||||
color: ${props => props.theme.tabs_font_hover};
|
|
||||||
}
|
|
||||||
background-color: ${props =>
|
|
||||||
// @ts-ignore
|
|
||||||
props.selected ? props.theme.tabs_background1 : 'inherit'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FileNameWrapper = styled.span`
|
|
||||||
margin-left: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CloseButton = styled(FontIcon)`
|
|
||||||
margin-left: 5px;
|
|
||||||
color: gray;
|
|
||||||
&:hover {
|
|
||||||
color: ${props => props.theme.tabs_font2};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function TabContextMenu({ close, closeAll, closeOthers, closeWithSameDb, closeWithOtherDb, props }) {
|
|
||||||
const { database } = props || {};
|
|
||||||
const { conid } = props || {};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={close}>Close</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={closeAll}>Close all</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={closeOthers}>Close others</DropDownMenuItem>
|
|
||||||
{conid && database && (
|
|
||||||
<DropDownMenuItem onClick={closeWithSameDb}>Close with same DB - {database}</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{conid && database && (
|
|
||||||
<DropDownMenuItem onClick={closeWithOtherDb}>Close with other DB than {database}</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTabDbName(tab) {
|
|
||||||
if (tab.props && tab.props.conid && tab.props.database) return tab.props.database;
|
|
||||||
if (tab.props && tab.props.archiveFolder) return tab.props.archiveFolder;
|
|
||||||
return '(no DB)';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTabDbKey(tab) {
|
|
||||||
if (tab.props && tab.props.conid && tab.props.database) return `database://${tab.props.database}-${tab.props.conid}`;
|
|
||||||
if (tab.props && tab.props.archiveFolder) return `archive://${tab.props.archiveFolder}`;
|
|
||||||
return '_no';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDbIcon(key) {
|
|
||||||
if (key.startsWith('database://')) return 'icon database';
|
|
||||||
if (key.startsWith('archive://')) return 'icon archive';
|
|
||||||
return 'icon file';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTooltip(tab) {
|
|
||||||
let res = tab.tooltip;
|
|
||||||
if (tab.props && tab.props.savedFilePath) {
|
|
||||||
if (res) res += '\n';
|
|
||||||
res += tab.props.savedFilePath;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TabsPanel() {
|
|
||||||
// const formatDbKey = (conid, database) => `${database}-${conid}`;
|
|
||||||
const theme = useTheme();
|
|
||||||
const showMenu = useShowMenu();
|
|
||||||
|
|
||||||
const tabs = useOpenedTabs();
|
|
||||||
const setOpenedTabs = useSetOpenedTabs();
|
|
||||||
const currentDb = useCurrentDatabase();
|
|
||||||
const setCurrentDb = useSetCurrentDatabase();
|
|
||||||
|
|
||||||
const { name, connection } = currentDb || {};
|
|
||||||
const currentDbKey = name && connection ? `database://${name}-${connection._id}` : '_no';
|
|
||||||
|
|
||||||
const handleTabClick = (e, tabid) => {
|
|
||||||
if (e.target.closest('.tabCloseButton')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpenedTabs(files => setSelectedTabFunc(files, tabid));
|
|
||||||
};
|
|
||||||
const closeTabFunc = closeCondition => tabid => {
|
|
||||||
setOpenedTabs(files => {
|
|
||||||
const active = files.find(x => x.tabid == tabid);
|
|
||||||
if (!active) return files;
|
|
||||||
|
|
||||||
const newFiles = files.map(x => ({
|
|
||||||
...x,
|
|
||||||
closedTime: x.closedTime || (closeCondition(x, active) ? new Date().getTime() : undefined),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (newFiles.find(x => x.selected && x.closedTime == null)) {
|
|
||||||
return newFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedIndex = _.findLastIndex(newFiles, x => x.closedTime == null);
|
|
||||||
|
|
||||||
return newFiles.map((x, index) => ({
|
|
||||||
...x,
|
|
||||||
selected: index == selectedIndex,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeTab = closeTabFunc((x, active) => x.tabid == active.tabid);
|
|
||||||
const closeAll = () => {
|
|
||||||
const closedTime = new Date().getTime();
|
|
||||||
setOpenedTabs(tabs =>
|
|
||||||
tabs.map(tab => ({
|
|
||||||
...tab,
|
|
||||||
closedTime: tab.closedTime || closedTime,
|
|
||||||
selected: false,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const closeWithSameDb = closeTabFunc(
|
|
||||||
(x, active) =>
|
|
||||||
_.get(x, 'props.conid') == _.get(active, 'props.conid') &&
|
|
||||||
_.get(x, 'props.database') == _.get(active, 'props.database')
|
|
||||||
);
|
|
||||||
const closeWithOtherDb = closeTabFunc(
|
|
||||||
(x, active) =>
|
|
||||||
_.get(x, 'props.conid') != _.get(active, 'props.conid') ||
|
|
||||||
_.get(x, 'props.database') != _.get(active, 'props.database')
|
|
||||||
);
|
|
||||||
const closeOthers = closeTabFunc((x, active) => x.tabid != active.tabid);
|
|
||||||
const handleMouseUp = (e, tabid) => {
|
|
||||||
if (e.button == 1) {
|
|
||||||
e.preventDefault();
|
|
||||||
closeTab(tabid);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleContextMenu = (event, tabid, props) => {
|
|
||||||
event.preventDefault();
|
|
||||||
showMenu(
|
|
||||||
event.pageX,
|
|
||||||
event.pageY,
|
|
||||||
<TabContextMenu
|
|
||||||
close={() => closeTab(tabid)}
|
|
||||||
closeAll={closeAll}
|
|
||||||
closeOthers={() => closeOthers(tabid)}
|
|
||||||
closeWithSameDb={() => closeWithSameDb(tabid)}
|
|
||||||
closeWithOtherDb={() => closeWithOtherDb(tabid)}
|
|
||||||
props={props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const electron = getElectron();
|
|
||||||
if (electron) {
|
|
||||||
const { ipcRenderer } = electron;
|
|
||||||
const activeTab = tabs.find(x => x.selected);
|
|
||||||
window['dbgate_activeTabId'] = activeTab ? activeTab.tabid : null;
|
|
||||||
ipcRenderer.send('update-menu');
|
|
||||||
}
|
|
||||||
}, [tabs]);
|
|
||||||
|
|
||||||
// console.log(
|
|
||||||
// 't',
|
|
||||||
// tabs.map(x => x.tooltip)
|
|
||||||
// );
|
|
||||||
const tabsWithDb = tabs
|
|
||||||
.filter(x => !x.closedTime)
|
|
||||||
.map(tab => ({
|
|
||||||
...tab,
|
|
||||||
tabDbName: getTabDbName(tab),
|
|
||||||
tabDbKey: getTabDbKey(tab),
|
|
||||||
}));
|
|
||||||
const tabsByDb = _.groupBy(tabsWithDb, 'tabDbKey');
|
|
||||||
const dbKeys = _.keys(tabsByDb).sort();
|
|
||||||
|
|
||||||
const handleSetDb = async props => {
|
|
||||||
const { conid, database } = props || {};
|
|
||||||
if (conid) {
|
|
||||||
const connection = await getConnectionInfo({ conid, database });
|
|
||||||
if (connection) {
|
|
||||||
setCurrentDb({ connection, name: database });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setCurrentDb(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{dbKeys.map(dbKey => (
|
|
||||||
<DbWrapperHandler key={dbKey}>
|
|
||||||
<DbNameWrapper
|
|
||||||
// @ts-ignore
|
|
||||||
selected={tabsByDb[dbKey][0].tabDbKey == currentDbKey}
|
|
||||||
onClick={() => handleSetDb(tabsByDb[dbKey][0].props)}
|
|
||||||
theme={theme}
|
|
||||||
>
|
|
||||||
<FontIcon icon={getDbIcon(dbKey)} /> {tabsByDb[dbKey][0].tabDbName}
|
|
||||||
</DbNameWrapper>
|
|
||||||
<DbGroupHandler>
|
|
||||||
{_.sortBy(tabsByDb[dbKey], ['title', 'tabid']).map(tab => (
|
|
||||||
<FileTabItem
|
|
||||||
{...tab}
|
|
||||||
title={buildTooltip(tab)}
|
|
||||||
key={tab.tabid}
|
|
||||||
theme={theme}
|
|
||||||
onClick={e => handleTabClick(e, tab.tabid)}
|
|
||||||
onMouseUp={e => handleMouseUp(e, tab.tabid)}
|
|
||||||
onContextMenu={e => handleContextMenu(e, tab.tabid, tab.props)}
|
|
||||||
>
|
|
||||||
{<FontIcon icon={tab.busy ? 'icon loading' : tab.icon} />}
|
|
||||||
<FileNameWrapper>{tab.title}</FileNameWrapper>
|
|
||||||
<CloseButton
|
|
||||||
icon="icon close"
|
|
||||||
className="tabCloseButton"
|
|
||||||
theme={theme}
|
|
||||||
onClick={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
closeTab(tab.tabid);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FileTabItem>
|
|
||||||
))}
|
|
||||||
</DbGroupHandler>
|
|
||||||
</DbWrapperHandler>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import { useShowMenu } from '../modals/showMenu';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const AppObjectDiv = styled.div`
|
|
||||||
padding: 5px;
|
|
||||||
${props =>
|
|
||||||
!props.disableHover &&
|
|
||||||
`
|
|
||||||
&:hover {
|
|
||||||
background-color: ${props.theme.left_background_blue[1]};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: ${props => (props.isBold ? 'bold' : 'normal')};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const IconWrap = styled.span`
|
|
||||||
margin-right: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StatusIconWrap = styled.span`
|
|
||||||
margin-left: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ExtInfoWrap = styled.span`
|
|
||||||
font-weight: normal;
|
|
||||||
margin-left: 5px;
|
|
||||||
color: ${props => props.theme.left_font3};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function AppObjectCore({
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
data,
|
|
||||||
onClick = undefined,
|
|
||||||
onClick2 = undefined,
|
|
||||||
onClick3 = undefined,
|
|
||||||
isBold = undefined,
|
|
||||||
isBusy = undefined,
|
|
||||||
prefix = undefined,
|
|
||||||
statusIcon = undefined,
|
|
||||||
extInfo = undefined,
|
|
||||||
statusTitle = undefined,
|
|
||||||
disableHover = false,
|
|
||||||
children = null,
|
|
||||||
Menu = undefined,
|
|
||||||
...other
|
|
||||||
}) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const showMenu = useShowMenu();
|
|
||||||
|
|
||||||
const handleContextMenu = event => {
|
|
||||||
if (!Menu) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
showMenu(event.pageX, event.pageY, <Menu data={data} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AppObjectDiv
|
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
onClick={() => {
|
|
||||||
if (onClick) onClick(data);
|
|
||||||
if (onClick2) onClick2(data);
|
|
||||||
if (onClick3) onClick3(data);
|
|
||||||
}}
|
|
||||||
theme={theme}
|
|
||||||
isBold={isBold}
|
|
||||||
draggable
|
|
||||||
onDragStart={e => {
|
|
||||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
|
||||||
}}
|
|
||||||
disableHover={disableHover}
|
|
||||||
{...other}
|
|
||||||
>
|
|
||||||
{prefix}
|
|
||||||
<IconWrap>{isBusy ? <FontIcon icon="icon loading" /> : <FontIcon icon={icon} />}</IconWrap>
|
|
||||||
{title}
|
|
||||||
{statusIcon && (
|
|
||||||
<StatusIconWrap>
|
|
||||||
<FontIcon icon={statusIcon} title={statusTitle} />
|
|
||||||
</StatusIconWrap>
|
|
||||||
)}
|
|
||||||
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
|
|
||||||
</AppObjectDiv>
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { ExpandIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const SubItemsDiv = styled.div`
|
|
||||||
margin-left: 28px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ExpandIconHolder2 = styled.span`
|
|
||||||
margin-right: 3px;
|
|
||||||
// position: relative;
|
|
||||||
// top: -3px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ExpandIconHolder = styled.span`
|
|
||||||
margin-right: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GroupDiv = styled.div`
|
|
||||||
user-select: none;
|
|
||||||
padding: 5px;
|
|
||||||
&:hover {
|
|
||||||
background-color: ${props => props.theme.left_background_blue[1]};
|
|
||||||
}
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: bold;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function AppObjectListItem({
|
|
||||||
AppObjectComponent,
|
|
||||||
data,
|
|
||||||
filter,
|
|
||||||
onObjectClick,
|
|
||||||
isExpandable,
|
|
||||||
SubItems,
|
|
||||||
getCommonProps,
|
|
||||||
expandOnClick,
|
|
||||||
ExpandIconComponent,
|
|
||||||
}) {
|
|
||||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
|
||||||
|
|
||||||
const expandable = data && isExpandable && isExpandable(data);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!expandable) {
|
|
||||||
setIsExpanded(false);
|
|
||||||
}
|
|
||||||
}, [expandable]);
|
|
||||||
|
|
||||||
let commonProps = {
|
|
||||||
prefix: SubItems ? (
|
|
||||||
<ExpandIconHolder2>
|
|
||||||
{expandable ? (
|
|
||||||
<ExpandIconComponent
|
|
||||||
isExpanded={isExpanded}
|
|
||||||
onClick={e => {
|
|
||||||
setIsExpanded(v => !v);
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ExpandIconComponent isBlank />
|
|
||||||
)}
|
|
||||||
</ExpandIconHolder2>
|
|
||||||
) : null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (SubItems && expandOnClick) {
|
|
||||||
commonProps.onClick2 = () => setIsExpanded(v => !v);
|
|
||||||
}
|
|
||||||
if (onObjectClick) {
|
|
||||||
commonProps.onClick3 = onObjectClick;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getCommonProps) {
|
|
||||||
commonProps = { ...commonProps, ...getCommonProps(data) };
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = <AppObjectComponent data={data} commonProps={commonProps} />;
|
|
||||||
if (SubItems && isExpanded) {
|
|
||||||
res = (
|
|
||||||
<>
|
|
||||||
{res}
|
|
||||||
<SubItemsDiv>
|
|
||||||
<SubItems data={data} filter={filter} />
|
|
||||||
</SubItemsDiv>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AppObjectGroup({ group, items }) {
|
|
||||||
const [isExpanded, setIsExpanded] = React.useState(true);
|
|
||||||
const theme = useTheme();
|
|
||||||
const filtered = items.filter(x => x.component);
|
|
||||||
let countText = filtered.length.toString();
|
|
||||||
if (filtered.length < items.length) countText += `/${items.length}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<GroupDiv onClick={() => setIsExpanded(!isExpanded)} theme={theme}>
|
|
||||||
<ExpandIconHolder>
|
|
||||||
<ExpandIcon isExpanded={isExpanded} />
|
|
||||||
</ExpandIconHolder>
|
|
||||||
{group} {items && `(${countText})`}
|
|
||||||
</GroupDiv>
|
|
||||||
{isExpanded && filtered.map(x => x.component)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppObjectList({
|
|
||||||
list,
|
|
||||||
AppObjectComponent,
|
|
||||||
SubItems = undefined,
|
|
||||||
onObjectClick = undefined,
|
|
||||||
filter = undefined,
|
|
||||||
groupFunc = undefined,
|
|
||||||
groupOrdered = undefined,
|
|
||||||
isExpandable = undefined,
|
|
||||||
getCommonProps = undefined,
|
|
||||||
expandOnClick = false,
|
|
||||||
ExpandIconComponent = ExpandIcon,
|
|
||||||
}) {
|
|
||||||
const createComponent = data => (
|
|
||||||
<AppObjectListItem
|
|
||||||
key={AppObjectComponent.extractKey(data)}
|
|
||||||
AppObjectComponent={AppObjectComponent}
|
|
||||||
data={data}
|
|
||||||
filter={filter}
|
|
||||||
onObjectClick={onObjectClick}
|
|
||||||
SubItems={SubItems}
|
|
||||||
isExpandable={isExpandable}
|
|
||||||
getCommonProps={getCommonProps}
|
|
||||||
expandOnClick={expandOnClick}
|
|
||||||
ExpandIconComponent={ExpandIconComponent}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (groupFunc) {
|
|
||||||
const listGrouped = _.compact(
|
|
||||||
(list || []).map(data => {
|
|
||||||
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
|
|
||||||
const component = matcher && !matcher(filter) ? null : createComponent(data);
|
|
||||||
const group = groupFunc(data);
|
|
||||||
return { group, data, component };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const groups = _.groupBy(listGrouped, 'group');
|
|
||||||
return (groupOrdered || _.keys(groups)).map(group => (
|
|
||||||
<AppObjectGroup key={group} group={group} items={groups[group]} />
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (list || []).map(data => {
|
|
||||||
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
|
|
||||||
if (matcher && !matcher(filter)) return null;
|
|
||||||
return createComponent(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import { filterName } from 'dbgate-datalib';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import { AppObjectCore } from './AppObjectCore';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
|
|
||||||
function openArchive(openNewTab, fileName, folderName) {
|
|
||||||
openNewTab({
|
|
||||||
title: fileName,
|
|
||||||
icon: 'img archive',
|
|
||||||
tooltip: `${folderName}\n${fileName}`,
|
|
||||||
tabComponent: 'ArchiveFileTab',
|
|
||||||
props: {
|
|
||||||
archiveFile: fileName,
|
|
||||||
archiveFolder: folderName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function Menu({ data }) {
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
const handleDelete = () => {
|
|
||||||
axios.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
|
|
||||||
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
|
|
||||||
};
|
|
||||||
const handleOpenRead = () => {
|
|
||||||
openArchive(openNewTab, data.fileName, data.folderName);
|
|
||||||
};
|
|
||||||
const handleOpenWrite = async () => {
|
|
||||||
// const resp = await axios.post('archive/load-free-table', { file: data.fileName, folder: data.folderName });
|
|
||||||
|
|
||||||
openNewTab({
|
|
||||||
title: data.fileName,
|
|
||||||
icon: 'img archive',
|
|
||||||
tabComponent: 'FreeTableTab',
|
|
||||||
props: {
|
|
||||||
initialArgs: {
|
|
||||||
functionName: 'archiveReader',
|
|
||||||
props: {
|
|
||||||
fileName: data.fileName,
|
|
||||||
folderName: data.folderName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
archiveFile: data.fileName,
|
|
||||||
archiveFolder: data.folderName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={handleOpenRead}>Open (readonly)</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={handleOpenWrite}>Open in free table editor</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ArchiveFileAppObject({ data, commonProps }) {
|
|
||||||
const { fileName, folderName } = data;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
const onClick = () => {
|
|
||||||
openArchive(openNewTab, fileName, folderName);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppObjectCore {...commonProps} data={data} title={fileName} icon="img archive" onClick={onClick} Menu={Menu} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArchiveFileAppObject.extractKey = data => data.fileName;
|
|
||||||
ArchiveFileAppObject.createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
|
|
||||||
|
|
||||||
export default ArchiveFileAppObject;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import { filterName } from 'dbgate-datalib';
|
|
||||||
import { AppObjectCore } from './AppObjectCore';
|
|
||||||
import { useCurrentArchive } from '../utility/globalState';
|
|
||||||
|
|
||||||
function Menu({ data }) {
|
|
||||||
const handleDelete = () => {
|
|
||||||
axios.post('archive/delete-folder', { folder: data.name });
|
|
||||||
};
|
|
||||||
return <>{data.name != 'default' && <DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ArchiveFolderAppObject({ data, commonProps }) {
|
|
||||||
const { name } = data;
|
|
||||||
const currentArchive = useCurrentArchive();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppObjectCore
|
|
||||||
{...commonProps}
|
|
||||||
data={data}
|
|
||||||
title={name}
|
|
||||||
icon="img archive-folder"
|
|
||||||
isBold={name == currentArchive}
|
|
||||||
Menu={Menu}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArchiveFolderAppObject.extractKey = data => data.name;
|
|
||||||
ArchiveFolderAppObject.createMatcher = data => filter => filterName(filter, data.name);
|
|
||||||
|
|
||||||
export default ArchiveFolderAppObject;
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import { useSetOpenedTabs } from '../utility/globalState';
|
|
||||||
import { AppObjectCore } from './AppObjectCore';
|
|
||||||
import { setSelectedTabFunc } from '../utility/common';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const InfoDiv = styled.div`
|
|
||||||
margin-left: 30px;
|
|
||||||
color: ${props => props.theme.left_font3};
|
|
||||||
`;
|
|
||||||
|
|
||||||
function Menu({ data }) {
|
|
||||||
const setOpenedTabs = useSetOpenedTabs();
|
|
||||||
const handleDelete = () => {
|
|
||||||
setOpenedTabs(tabs => tabs.filter(x => x.tabid != data.tabid));
|
|
||||||
};
|
|
||||||
const handleDeleteOlder = () => {
|
|
||||||
setOpenedTabs(tabs => tabs.filter(x => !x.closedTime || x.closedTime >= data.closedTime));
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={handleDeleteOlder}>Delete older</DropDownMenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClosedTabAppObject({ data, commonProps }) {
|
|
||||||
const { tabid, props, selected, icon, title, closedTime, busy } = data;
|
|
||||||
const setOpenedTabs = useSetOpenedTabs();
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
setOpenedTabs(files =>
|
|
||||||
setSelectedTabFunc(
|
|
||||||
files.map(x => ({
|
|
||||||
...x,
|
|
||||||
closedTime: x.tabid == tabid ? undefined : x.closedTime,
|
|
||||||
})),
|
|
||||||
tabid
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppObjectCore
|
|
||||||
{...commonProps}
|
|
||||||
data={data}
|
|
||||||
title={`${title} ${moment(closedTime).fromNow()}`}
|
|
||||||
icon={icon}
|
|
||||||
isBold={!!selected}
|
|
||||||
onClick={onClick}
|
|
||||||
isBusy={busy}
|
|
||||||
Menu={Menu}
|
|
||||||
>
|
|
||||||
{data.props && data.props.database && (
|
|
||||||
<InfoDiv theme={theme}>
|
|
||||||
<FontIcon icon="icon database" /> {data.props.database}
|
|
||||||
</InfoDiv>
|
|
||||||
)}
|
|
||||||
{data.contentPreview && <InfoDiv theme={theme}>{data.contentPreview}</InfoDiv>}
|
|
||||||
</AppObjectCore>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClosedTabAppObject.extractKey = data => data.tabid;
|
|
||||||
|
|
||||||
export default ClosedTabAppObject;
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import ConnectionModal from '../modals/ConnectionModal';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import { filterName } from 'dbgate-datalib';
|
|
||||||
import ConfirmModal from '../modals/ConfirmModal';
|
|
||||||
import CreateDatabaseModal from '../modals/CreateDatabaseModal';
|
|
||||||
import { useCurrentDatabase, useOpenedConnections, useSetOpenedConnections } from '../utility/globalState';
|
|
||||||
import { AppObjectCore } from './AppObjectCore';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import { useConfig } from '../utility/metadataLoaders';
|
|
||||||
import useExtensions from '../utility/useExtensions';
|
|
||||||
|
|
||||||
function Menu({ data }) {
|
|
||||||
const openedConnections = useOpenedConnections();
|
|
||||||
const setOpenedConnections = useSetOpenedConnections();
|
|
||||||
const showModal = useShowModal();
|
|
||||||
const config = useConfig();
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
|
||||||
showModal(modalState => <ConnectionModal modalState={modalState} connection={data} />);
|
|
||||||
};
|
|
||||||
const handleDelete = () => {
|
|
||||||
showModal(modalState => (
|
|
||||||
<ConfirmModal
|
|
||||||
modalState={modalState}
|
|
||||||
message={`Really delete connection ${data.displayName || data.server}?`}
|
|
||||||
onConfirm={() => axios.post('connections/delete', data)}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
const handleCreateDatabase = () => {
|
|
||||||
showModal(modalState => <CreateDatabaseModal modalState={modalState} conid={data._id} />);
|
|
||||||
};
|
|
||||||
const handleRefresh = () => {
|
|
||||||
axios.post('server-connections/refresh', { conid: data._id });
|
|
||||||
};
|
|
||||||
const handleDisconnect = () => {
|
|
||||||
setOpenedConnections(list => list.filter(x => x != data._id));
|
|
||||||
};
|
|
||||||
const handleConnect = () => {
|
|
||||||
setOpenedConnections(list => _.uniq([...list, data._id]));
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{config.runAsPortal == false && (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={handleEdit}>Edit</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!openedConnections.includes(data._id) && <DropDownMenuItem onClick={handleConnect}>Connect</DropDownMenuItem>}
|
|
||||||
{openedConnections.includes(data._id) && data.status && (
|
|
||||||
<DropDownMenuItem onClick={handleRefresh}>Refresh</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{openedConnections.includes(data._id) && (
|
|
||||||
<DropDownMenuItem onClick={handleDisconnect}>Disconnect</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{openedConnections.includes(data._id) && (
|
|
||||||
<DropDownMenuItem onClick={handleCreateDatabase}>Create database</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConnectionAppObject({ data, commonProps }) {
|
|
||||||
const { _id, server, displayName, engine, status } = data;
|
|
||||||
const openedConnections = useOpenedConnections();
|
|
||||||
const setOpenedConnections = useSetOpenedConnections();
|
|
||||||
const currentDatabase = useCurrentDatabase();
|
|
||||||
const extensions = useExtensions();
|
|
||||||
|
|
||||||
const isBold = _.get(currentDatabase, 'connection._id') == _id;
|
|
||||||
const onClick = () => setOpenedConnections(c => _.uniq([...c, _id]));
|
|
||||||
|
|
||||||
let statusIcon = null;
|
|
||||||
let statusTitle = null;
|
|
||||||
|
|
||||||
let extInfo = null;
|
|
||||||
if (extensions.drivers.find(x => x.engine == engine)) {
|
|
||||||
const match = (engine || '').match(/^([^@]*)@/);
|
|
||||||
extInfo = match ? match[1] : engine;
|
|
||||||
} else {
|
|
||||||
extInfo = engine;
|
|
||||||
statusIcon = 'img warn';
|
|
||||||
statusTitle = `Engine driver ${engine} not found, review installed plugins and change engine in edit connection dialog`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openedConnections.includes(_id)) {
|
|
||||||
if (!status) statusIcon = 'icon loading';
|
|
||||||
else if (status.name == 'pending') statusIcon = 'icon loading';
|
|
||||||
else if (status.name == 'ok') statusIcon = 'img ok';
|
|
||||||
else statusIcon = 'img error';
|
|
||||||
if (status && status.name == 'error') {
|
|
||||||
statusTitle = status.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppObjectCore
|
|
||||||
{...commonProps}
|
|
||||||
title={displayName || server}
|
|
||||||
icon="img server"
|
|
||||||
data={data}
|
|
||||||
statusIcon={statusIcon}
|
|
||||||
statusTitle={statusTitle}
|
|
||||||
extInfo={extInfo}
|
|
||||||
isBold={isBold}
|
|
||||||
onClick={onClick}
|
|
||||||
Menu={Menu}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionAppObject.extractKey = data => data._id;
|
|
||||||
ConnectionAppObject.createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server);
|
|
||||||
|
|
||||||
export default ConnectionAppObject;
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import ImportExportModal from '../modals/ImportExportModal';
|
|
||||||
import { getDefaultFileFormat } from '../utility/fileformats';
|
|
||||||
import { useCurrentDatabase } from '../utility/globalState';
|
|
||||||
import { AppObjectCore } from './AppObjectCore';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import useExtensions from '../utility/useExtensions';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
|
|
||||||
function Menu({ data }) {
|
|
||||||
const { connection, name } = data;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
const extensions = useExtensions();
|
|
||||||
const showModal = useShowModal();
|
|
||||||
|
|
||||||
const tooltip = `${connection.displayName || connection.server}\n${name}`;
|
|
||||||
|
|
||||||
const handleNewQuery = () => {
|
|
||||||
openNewTab({
|
|
||||||
title: 'Query #',
|
|
||||||
icon: 'img sql-file',
|
|
||||||
tooltip,
|
|
||||||
tabComponent: 'QueryTab',
|
|
||||||
props: {
|
|
||||||
conid: connection._id,
|
|
||||||
database: name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImport = () => {
|
|
||||||
showModal(modalState => (
|
|
||||||
<ImportExportModal
|
|
||||||
modalState={modalState}
|
|
||||||
initialValues={{
|
|
||||||
sourceStorageType: getDefaultFileFormat(extensions).storageType,
|
|
||||||
targetStorageType: 'database',
|
|
||||||
targetConnectionId: connection._id,
|
|
||||||
targetDatabaseName: name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = () => {
|
|
||||||
showModal(modalState => (
|
|
||||||
<ImportExportModal
|
|
||||||
modalState={modalState}
|
|
||||||
initialValues={{
|
|
||||||
targetStorageType: getDefaultFileFormat(extensions).storageType,
|
|
||||||
sourceStorageType: 'database',
|
|
||||||
sourceConnectionId: connection._id,
|
|
||||||
sourceDatabaseName: name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={handleNewQuery}>New query</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={handleImport}>Import</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={handleExport}>Export</DropDownMenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DatabaseAppObject({ data, commonProps }) {
|
|
||||||
const { name, connection } = data;
|
|
||||||
const currentDatabase = useCurrentDatabase();
|
|
||||||
return (
|
|
||||||
<AppObjectCore
|
|
||||||
{...commonProps}
|
|
||||||
data={data}
|
|
||||||
title={name}
|
|
||||||
icon="img database"
|
|
||||||
isBold={
|
|
||||||
_.get(currentDatabase, 'connection._id') == _.get(connection, '_id') && _.get(currentDatabase, 'name') == name
|
|
||||||
}
|
|
||||||
Menu={Menu}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseAppObject.extractKey = props => props.name;
|
|
||||||
|
|
||||||
export default DatabaseAppObject;
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
|
||||||
import fullDisplayName from '../utility/fullDisplayName';
|
|
||||||
import { filterName } from 'dbgate-datalib';
|
|
||||||
import ImportExportModal from '../modals/ImportExportModal';
|
|
||||||
import { useSetOpenedTabs } from '../utility/globalState';
|
|
||||||
import { AppObjectCore } from './AppObjectCore';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
|
||||||
import useExtensions from '../utility/useExtensions';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
import uuidv1 from 'uuid/v1';
|
|
||||||
import { AppObjectList } from './AppObjectList';
|
|
||||||
|
|
||||||
const icons = {
|
|
||||||
tables: 'img table',
|
|
||||||
views: 'img view',
|
|
||||||
procedures: 'img procedure',
|
|
||||||
functions: 'img function',
|
|
||||||
};
|
|
||||||
|
|
||||||
const menus = {
|
|
||||||
tables: [
|
|
||||||
{
|
|
||||||
label: 'Open data',
|
|
||||||
tab: 'TableDataTab',
|
|
||||||
forceNewTab: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open form',
|
|
||||||
tab: 'TableDataTab',
|
|
||||||
forceNewTab: true,
|
|
||||||
initialData: {
|
|
||||||
grid: {
|
|
||||||
isFormView: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open structure',
|
|
||||||
tab: 'TableStructureTab',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Query designer',
|
|
||||||
isQueryDesigner: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isDivider: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Export',
|
|
||||||
isExport: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open in free table editor',
|
|
||||||
isOpenFreeTable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open active chart',
|
|
||||||
isActiveChart: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isDivider: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'SQL: CREATE TABLE',
|
|
||||||
sqlTemplate: 'CREATE TABLE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
views: [
|
|
||||||
{
|
|
||||||
label: 'Open data',
|
|
||||||
tab: 'ViewDataTab',
|
|
||||||
forceNewTab: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open structure',
|
|
||||||
tab: 'TableStructureTab',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Query designer',
|
|
||||||
isQueryDesigner: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isDivider: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Export',
|
|
||||||
isExport: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open in free table editor',
|
|
||||||
isOpenFreeTable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Open active chart',
|
|
||||||
isActiveChart: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isDivider: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'SQL: CREATE VIEW',
|
|
||||||
sqlTemplate: 'CREATE OBJECT',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'SQL: CREATE TABLE',
|
|
||||||
sqlTemplate: 'CREATE TABLE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
procedures: [
|
|
||||||
{
|
|
||||||
label: 'SQL: CREATE PROCEDURE',
|
|
||||||
sqlTemplate: 'CREATE OBJECT',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'SQL: EXECUTE',
|
|
||||||
sqlTemplate: 'EXECUTE PROCEDURE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
functions: [
|
|
||||||
{
|
|
||||||
label: 'SQL: CREATE FUNCTION',
|
|
||||||
sqlTemplate: 'CREATE OBJECT',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultTabs = {
|
|
||||||
tables: 'TableDataTab',
|
|
||||||
views: 'ViewDataTab',
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function openDatabaseObjectDetail(
|
|
||||||
openNewTab,
|
|
||||||
tabComponent,
|
|
||||||
sqlTemplate,
|
|
||||||
{ schemaName, pureName, conid, database, objectTypeField },
|
|
||||||
forceNewTab,
|
|
||||||
initialData
|
|
||||||
) {
|
|
||||||
const connection = await getConnectionInfo({ conid });
|
|
||||||
const tooltip = `${connection.displayName || connection.server}\n${database}\n${fullDisplayName({
|
|
||||||
schemaName,
|
|
||||||
pureName,
|
|
||||||
})}`;
|
|
||||||
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: sqlTemplate ? 'Query #' : pureName,
|
|
||||||
tooltip,
|
|
||||||
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
|
|
||||||
tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
|
|
||||||
props: {
|
|
||||||
schemaName,
|
|
||||||
pureName,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
objectTypeField,
|
|
||||||
initialArgs: sqlTemplate ? { sqlTemplate } : null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initialData,
|
|
||||||
{ forceNewTab }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Menu({ data }) {
|
|
||||||
const showModal = useShowModal();
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
const extensions = useExtensions();
|
|
||||||
|
|
||||||
const getDriver = async () => {
|
|
||||||
const conn = await getConnectionInfo(data);
|
|
||||||
if (!conn) return;
|
|
||||||
const driver = findEngineDriver(conn, extensions);
|
|
||||||
return driver;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{menus[data.objectTypeField].map(menu =>
|
|
||||||
menu.isDivider ? (
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
) : (
|
|
||||||
<DropDownMenuItem
|
|
||||||
key={menu.label}
|
|
||||||
onClick={async () => {
|
|
||||||
if (menu.isExport) {
|
|
||||||
showModal(modalState => (
|
|
||||||
<ImportExportModal
|
|
||||||
modalState={modalState}
|
|
||||||
initialValues={{
|
|
||||||
sourceStorageType: 'database',
|
|
||||||
sourceConnectionId: data.conid,
|
|
||||||
sourceDatabaseName: data.database,
|
|
||||||
sourceSchemaName: data.schemaName,
|
|
||||||
sourceList: [data.pureName],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
} else if (menu.isOpenFreeTable) {
|
|
||||||
const coninfo = await getConnectionInfo(data);
|
|
||||||
openNewTab({
|
|
||||||
title: data.pureName,
|
|
||||||
icon: 'img free-table',
|
|
||||||
tabComponent: 'FreeTableTab',
|
|
||||||
props: {
|
|
||||||
initialArgs: {
|
|
||||||
functionName: 'tableReader',
|
|
||||||
props: {
|
|
||||||
connection: {
|
|
||||||
...coninfo,
|
|
||||||
database: data.database,
|
|
||||||
},
|
|
||||||
schemaName: data.schemaName,
|
|
||||||
pureName: data.pureName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (menu.isActiveChart) {
|
|
||||||
const driver = await getDriver();
|
|
||||||
const dmp = driver.createDumper();
|
|
||||||
dmp.put('^select * from %f', data);
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: data.pureName,
|
|
||||||
icon: 'img chart',
|
|
||||||
tabComponent: 'ChartTab',
|
|
||||||
props: {
|
|
||||||
conid: data.conid,
|
|
||||||
database: data.database,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
editor: {
|
|
||||||
config: { chartType: 'bar' },
|
|
||||||
sql: dmp.s,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (menu.isQueryDesigner) {
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: 'Query #',
|
|
||||||
icon: 'img query-design',
|
|
||||||
tabComponent: 'QueryDesignTab',
|
|
||||||
props: {
|
|
||||||
conid: data.conid,
|
|
||||||
database: data.database,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
editor: {
|
|
||||||
tables: [
|
|
||||||
{
|
|
||||||
...data,
|
|
||||||
designerId: uuidv1(),
|
|
||||||
left: 50,
|
|
||||||
top: 50,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
openDatabaseObjectDetail(
|
|
||||||
openNewTab,
|
|
||||||
menu.tab,
|
|
||||||
menu.sqlTemplate,
|
|
||||||
data,
|
|
||||||
menu.forceNewTab,
|
|
||||||
menu.initialData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{menu.label}
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DatabaseObjectAppObject({ data, commonProps }) {
|
|
||||||
const { conid, database, pureName, schemaName, objectTypeField } = data;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
const onClick = ({ schemaName, pureName }) => {
|
|
||||||
openDatabaseObjectDetail(
|
|
||||||
openNewTab,
|
|
||||||
defaultTabs[objectTypeField],
|
|
||||||
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
|
|
||||||
{
|
|
||||||
schemaName,
|
|
||||||
pureName,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
objectTypeField,
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppObjectCore
|
|
||||||
{...commonProps}
|
|
||||||
data={data}
|
|
||||||
title={schemaName ? `${schemaName}.${pureName}` : pureName}
|
|
||||||
icon={icons[objectTypeField]}
|
|
||||||
onClick={onClick}
|
|
||||||
Menu={Menu}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseObjectAppObject.extractKey = ({ schemaName, pureName }) =>
|
|
||||||
schemaName ? `${schemaName}.${pureName}` : pureName;
|
|
||||||
|
|
||||||
DatabaseObjectAppObject.createMatcher = ({ pureName }) => filter => filterName(filter, pureName);
|
|
||||||
|
|
||||||
export default DatabaseObjectAppObject;
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import FavoriteModal from '../modals/FavoriteModal';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import { copyTextToClipboard } from '../utility/clipboard';
|
|
||||||
import getElectron from '../utility/getElectron';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
import { SavedFileAppObjectBase } from './SavedFileAppObject';
|
|
||||||
|
|
||||||
export function useOpenFavorite() {
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
const openFavorite = React.useCallback(
|
|
||||||
async favorite => {
|
|
||||||
const { icon, tabComponent, title, props, tabdata } = favorite;
|
|
||||||
let tabdataNew = tabdata;
|
|
||||||
if (props.savedFile) {
|
|
||||||
const resp = await axios.post('files/load', {
|
|
||||||
folder: props.savedFolder,
|
|
||||||
file: props.savedFile,
|
|
||||||
format: props.savedFormat,
|
|
||||||
});
|
|
||||||
tabdataNew = {
|
|
||||||
...tabdata,
|
|
||||||
editor: resp.data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title,
|
|
||||||
icon: icon || 'img favorite',
|
|
||||||
props,
|
|
||||||
tabComponent,
|
|
||||||
},
|
|
||||||
tabdataNew
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[openNewTab]
|
|
||||||
);
|
|
||||||
|
|
||||||
return openFavorite;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FavoriteFileAppObject({ data, commonProps }) {
|
|
||||||
const { icon, tabComponent, title, props, tabdata, urlPath } = data;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
const showModal = useShowModal();
|
|
||||||
const openFavorite = useOpenFavorite();
|
|
||||||
const electron = getElectron();
|
|
||||||
|
|
||||||
const editFavorite = () => {
|
|
||||||
showModal(modalState => <FavoriteModal modalState={modalState} editingData={data} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
const editFavoriteJson = async () => {
|
|
||||||
const resp = await axios.post('files/load', {
|
|
||||||
folder: 'favorites',
|
|
||||||
file: data.file,
|
|
||||||
format: 'text',
|
|
||||||
});
|
|
||||||
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
icon: 'icon favorite',
|
|
||||||
title,
|
|
||||||
tabComponent: 'FavoriteEditorTab',
|
|
||||||
props: {
|
|
||||||
savedFile: data.file,
|
|
||||||
savedFormat: 'text',
|
|
||||||
savedFolder: 'favorites',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ editor: JSON.stringify(JSON.parse(resp.data), null, 2) }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyLink = () => {
|
|
||||||
copyTextToClipboard(`${document.location.origin}#favorite=${urlPath}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SavedFileAppObjectBase
|
|
||||||
data={data}
|
|
||||||
commonProps={commonProps}
|
|
||||||
format="json"
|
|
||||||
icon={icon || 'img favorite'}
|
|
||||||
title={title}
|
|
||||||
disableRename
|
|
||||||
onLoad={async data => {
|
|
||||||
openFavorite(data);
|
|
||||||
}}
|
|
||||||
menuExt={
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={editFavorite}>Edit</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={editFavoriteJson}>Edit JSON definition</DropDownMenuItem>
|
|
||||||
{!electron && urlPath && <DropDownMenuItem onClick={copyLink}>Copy link</DropDownMenuItem>}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FavoriteFileAppObject.extractKey = data => data.file;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import { filterName } from 'dbgate-datalib';
|
|
||||||
import { AppObjectCore } from './AppObjectCore';
|
|
||||||
|
|
||||||
function MacroAppObject({ data, commonProps }) {
|
|
||||||
const { name, type, title, group } = data;
|
|
||||||
|
|
||||||
return <AppObjectCore {...commonProps} data={data} title={title} icon={'img macro'} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
MacroAppObject.extractKey = data => data.name;
|
|
||||||
MacroAppObject.createMatcher = ({ name, title }) => filter => filterName(filter, name, title);
|
|
||||||
|
|
||||||
export default MacroAppObject;
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import { AppObjectCore } from './AppObjectCore';
|
|
||||||
import useNewQuery from '../query/useNewQuery';
|
|
||||||
import { useCurrentDatabase } from '../utility/globalState';
|
|
||||||
import ScriptWriter from '../impexp/ScriptWriter';
|
|
||||||
import { extractPackageName } from 'dbgate-tools';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import InputTextModal from '../modals/InputTextModal';
|
|
||||||
import useHasPermission from '../utility/useHasPermission';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
import ConfirmModal from '../modals/ConfirmModal';
|
|
||||||
|
|
||||||
function Menu({ data, menuExt = null, title = undefined, disableRename = false }) {
|
|
||||||
const hasPermission = useHasPermission();
|
|
||||||
const showModal = useShowModal();
|
|
||||||
const handleDelete = () => {
|
|
||||||
showModal(modalState => (
|
|
||||||
<ConfirmModal
|
|
||||||
modalState={modalState}
|
|
||||||
message={`Really delete file ${title || data.file}?`}
|
|
||||||
onConfirm={() => {
|
|
||||||
axios.post('files/delete', data);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
const handleRename = () => {
|
|
||||||
showModal(modalState => (
|
|
||||||
<InputTextModal
|
|
||||||
modalState={modalState}
|
|
||||||
value={data.file}
|
|
||||||
label="New file name"
|
|
||||||
header="Rename file"
|
|
||||||
onConfirm={newFile => {
|
|
||||||
axios.post('files/rename', { ...data, newFile });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{hasPermission(`files/${data.folder}/write`) && (
|
|
||||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{hasPermission(`files/${data.folder}/write`) && !disableRename && (
|
|
||||||
<DropDownMenuItem onClick={handleRename}>Rename</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{menuExt}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SavedFileAppObjectBase({
|
|
||||||
data,
|
|
||||||
commonProps,
|
|
||||||
format,
|
|
||||||
icon,
|
|
||||||
onLoad,
|
|
||||||
title = undefined,
|
|
||||||
menuExt = null,
|
|
||||||
disableRename = false,
|
|
||||||
}) {
|
|
||||||
const { file, folder } = data;
|
|
||||||
|
|
||||||
const onClick = async () => {
|
|
||||||
const resp = await axios.post('files/load', { folder, file, format });
|
|
||||||
onLoad(resp.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppObjectCore
|
|
||||||
{...commonProps}
|
|
||||||
data={data}
|
|
||||||
title={title || file}
|
|
||||||
icon={icon}
|
|
||||||
onClick={onClick}
|
|
||||||
Menu={props => <Menu {...props} menuExt={menuExt} title={title} disableRename={disableRename} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SavedSqlFileAppObject({ data, commonProps }) {
|
|
||||||
const { file, folder } = data;
|
|
||||||
const newQuery = useNewQuery();
|
|
||||||
const currentDatabase = useCurrentDatabase();
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
const connection = _.get(currentDatabase, 'connection');
|
|
||||||
const database = _.get(currentDatabase, 'name');
|
|
||||||
|
|
||||||
const handleGenerateExecute = () => {
|
|
||||||
const script = new ScriptWriter();
|
|
||||||
const conn = {
|
|
||||||
..._.omit(connection, ['displayName', '_id']),
|
|
||||||
database,
|
|
||||||
};
|
|
||||||
script.put(`const sql = await dbgateApi.loadFile('${folder}/${file}');`);
|
|
||||||
script.put(`await dbgateApi.executeQuery({ sql, connection: ${JSON.stringify(conn)} });`);
|
|
||||||
// @ts-ignore
|
|
||||||
script.requirePackage(extractPackageName(conn.engine));
|
|
||||||
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: 'Shell #',
|
|
||||||
icon: 'img shell',
|
|
||||||
tabComponent: 'ShellTab',
|
|
||||||
},
|
|
||||||
{ editor: script.getScript() }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SavedFileAppObjectBase
|
|
||||||
data={data}
|
|
||||||
commonProps={commonProps}
|
|
||||||
format="text"
|
|
||||||
icon="img sql-file"
|
|
||||||
menuExt={
|
|
||||||
connection && database ? (
|
|
||||||
<DropDownMenuItem onClick={handleGenerateExecute}>Generate shell execute</DropDownMenuItem>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onLoad={data => {
|
|
||||||
newQuery({
|
|
||||||
title: file,
|
|
||||||
initialData: data,
|
|
||||||
// @ts-ignore
|
|
||||||
savedFile: file,
|
|
||||||
savedFolder: 'sql',
|
|
||||||
savedFormat: 'text',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SavedShellFileAppObject({ data, commonProps }) {
|
|
||||||
const { file, folder } = data;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SavedFileAppObjectBase
|
|
||||||
data={data}
|
|
||||||
commonProps={commonProps}
|
|
||||||
format="text"
|
|
||||||
icon="img shell"
|
|
||||||
onLoad={data => {
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: file,
|
|
||||||
icon: 'img shell',
|
|
||||||
tabComponent: 'ShellTab',
|
|
||||||
props: {
|
|
||||||
savedFile: file,
|
|
||||||
savedFolder: 'shell',
|
|
||||||
savedFormat: 'text',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ editor: data }
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SavedChartFileAppObject({ data, commonProps }) {
|
|
||||||
const { file, folder } = data;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
const currentDatabase = useCurrentDatabase();
|
|
||||||
|
|
||||||
const connection = _.get(currentDatabase, 'connection') || {};
|
|
||||||
const database = _.get(currentDatabase, 'name');
|
|
||||||
|
|
||||||
const tooltip = `${connection.displayName || connection.server}\n${database}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SavedFileAppObjectBase
|
|
||||||
data={data}
|
|
||||||
commonProps={commonProps}
|
|
||||||
format="json"
|
|
||||||
icon="img chart"
|
|
||||||
onLoad={data => {
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: file,
|
|
||||||
icon: 'img chart',
|
|
||||||
tooltip,
|
|
||||||
props: {
|
|
||||||
conid: connection._id,
|
|
||||||
database,
|
|
||||||
savedFile: file,
|
|
||||||
savedFolder: 'charts',
|
|
||||||
savedFormat: 'json',
|
|
||||||
},
|
|
||||||
tabComponent: 'ChartTab',
|
|
||||||
},
|
|
||||||
{ editor: data }
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SavedQueryFileAppObject({ data, commonProps }) {
|
|
||||||
const { file, folder } = data;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
const currentDatabase = useCurrentDatabase();
|
|
||||||
|
|
||||||
const connection = _.get(currentDatabase, 'connection') || {};
|
|
||||||
const database = _.get(currentDatabase, 'name');
|
|
||||||
|
|
||||||
const tooltip = `${connection.displayName || connection.server}\n${database}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SavedFileAppObjectBase
|
|
||||||
data={data}
|
|
||||||
commonProps={commonProps}
|
|
||||||
format="json"
|
|
||||||
icon="img query-design"
|
|
||||||
onLoad={data => {
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: file,
|
|
||||||
icon: 'img query-design',
|
|
||||||
tooltip,
|
|
||||||
props: {
|
|
||||||
conid: connection._id,
|
|
||||||
database,
|
|
||||||
savedFile: file,
|
|
||||||
savedFolder: 'query',
|
|
||||||
savedFormat: 'json',
|
|
||||||
},
|
|
||||||
tabComponent: 'QueryDesignTab',
|
|
||||||
},
|
|
||||||
{ editor: data }
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SavedMarkdownFileAppObject({ data, commonProps }) {
|
|
||||||
const { file, folder } = data;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
const showPage = () => {
|
|
||||||
openNewTab({
|
|
||||||
title: file,
|
|
||||||
icon: 'img markdown',
|
|
||||||
tabComponent: 'MarkdownViewTab',
|
|
||||||
props: {
|
|
||||||
savedFile: file,
|
|
||||||
savedFolder: 'markdown',
|
|
||||||
savedFormat: 'text',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<SavedFileAppObjectBase
|
|
||||||
data={data}
|
|
||||||
commonProps={commonProps}
|
|
||||||
format="text"
|
|
||||||
icon="img markdown"
|
|
||||||
onLoad={data => {
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: file,
|
|
||||||
icon: 'img markdown',
|
|
||||||
tabComponent: 'MarkdownEditorTab',
|
|
||||||
props: {
|
|
||||||
savedFile: file,
|
|
||||||
savedFolder: 'markdown',
|
|
||||||
savedFormat: 'text',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ editor: data }
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
menuExt={<DropDownMenuItem onClick={showPage}>Show page</DropDownMenuItem>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SavedFileAppObject({ data, commonProps }) {
|
|
||||||
const { folder } = data;
|
|
||||||
const folderTypes = {
|
|
||||||
sql: SavedSqlFileAppObject,
|
|
||||||
shell: SavedShellFileAppObject,
|
|
||||||
charts: SavedChartFileAppObject,
|
|
||||||
markdown: SavedMarkdownFileAppObject,
|
|
||||||
query: SavedQueryFileAppObject,
|
|
||||||
};
|
|
||||||
const AppObject = folderTypes[folder];
|
|
||||||
if (AppObject) {
|
|
||||||
return <AppObject data={data} commonProps={commonProps} />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
[
|
|
||||||
SavedSqlFileAppObject,
|
|
||||||
SavedShellFileAppObject,
|
|
||||||
SavedChartFileAppObject,
|
|
||||||
SavedMarkdownFileAppObject,
|
|
||||||
SavedFileAppObject,
|
|
||||||
].forEach(fn => {
|
|
||||||
// @ts-ignore
|
|
||||||
fn.extractKey = data => data.file;
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { findForeignKeyForColumn } from 'dbgate-tools';
|
|
||||||
import React from 'react';
|
|
||||||
import { getColumnIcon } from '../datagrid/ColumnLabel';
|
|
||||||
import { AppObjectCore } from './AppObjectCore';
|
|
||||||
import { AppObjectList } from './AppObjectList';
|
|
||||||
|
|
||||||
function ColumnAppObject({ data, commonProps }) {
|
|
||||||
const { columnName, dataType, foreignKey } = data;
|
|
||||||
let extInfo = dataType;
|
|
||||||
if (foreignKey) extInfo += ` -> ${foreignKey.refTableName}`;
|
|
||||||
return (
|
|
||||||
<AppObjectCore
|
|
||||||
{...commonProps}
|
|
||||||
data={data}
|
|
||||||
title={columnName}
|
|
||||||
extInfo={extInfo}
|
|
||||||
icon={getColumnIcon(data, true)}
|
|
||||||
disableHover
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ColumnAppObject.extractKey = ({ columnName }) => columnName;
|
|
||||||
|
|
||||||
export default function SubColumnParamList({ data }) {
|
|
||||||
const { columns } = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppObjectList
|
|
||||||
list={(columns || []).map(col => ({
|
|
||||||
...col,
|
|
||||||
foreignKey: findForeignKeyForColumn(data, col),
|
|
||||||
}))}
|
|
||||||
AppObjectComponent={ColumnAppObject}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { SelectField } from '../utility/inputs';
|
|
||||||
import ErrorInfo from '../widgets/ErrorInfo';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { TextCellViewWrap, TextCellViewNoWrap } from './TextCellView';
|
|
||||||
import JsonCellView from './JsonCellDataView';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const Toolbar = styled.div`
|
|
||||||
display: flex;
|
|
||||||
background: ${props => props.theme.toolbar_background};
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MainWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DataWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const formats = [
|
|
||||||
{
|
|
||||||
type: 'textWrap',
|
|
||||||
title: 'Text (wrap)',
|
|
||||||
Component: TextCellViewWrap,
|
|
||||||
single: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
title: 'Text (no wrap)',
|
|
||||||
Component: TextCellViewNoWrap,
|
|
||||||
single: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'json',
|
|
||||||
title: 'Json',
|
|
||||||
Component: JsonCellView,
|
|
||||||
single: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function autodetect(selection, grider, value) {
|
|
||||||
if (_.isString(value)) {
|
|
||||||
if (value.startsWith('[') || value.startsWith('{')) return 'json';
|
|
||||||
}
|
|
||||||
return 'textWrap';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CellDataView({ selection = undefined, grider = undefined, selectedValue = undefined }) {
|
|
||||||
const [selectedFormatType, setSelectedFormatType] = React.useState('autodetect');
|
|
||||||
const theme = useTheme();
|
|
||||||
let value = null;
|
|
||||||
if (grider && selection && selection.length == 1) {
|
|
||||||
const rowData = grider.getRowData(selection[0].row);
|
|
||||||
const { column } = selection[0];
|
|
||||||
if (rowData) value = rowData[column];
|
|
||||||
}
|
|
||||||
if (selectedValue) {
|
|
||||||
value = selectedValue;
|
|
||||||
}
|
|
||||||
const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]);
|
|
||||||
const autodetectFormat = formats.find(x => x.type == autodetectFormatType);
|
|
||||||
|
|
||||||
const usedFormatType = selectedFormatType == 'autodetect' ? autodetectFormatType : selectedFormatType;
|
|
||||||
const usedFormat = formats.find(x => x.type == usedFormatType);
|
|
||||||
|
|
||||||
const { Component } = usedFormat || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MainWrapper>
|
|
||||||
<Toolbar theme={theme}>
|
|
||||||
Format:
|
|
||||||
<SelectField value={selectedFormatType} onChange={e => setSelectedFormatType(e.target.value)}>
|
|
||||||
<option value="autodetect">Autodetect - {autodetectFormat.title}</option>
|
|
||||||
|
|
||||||
{formats.map(fmt => (
|
|
||||||
<option value={fmt.type} key={fmt.type}>
|
|
||||||
{fmt.title}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</SelectField>
|
|
||||||
</Toolbar>
|
|
||||||
|
|
||||||
<DataWrapper>
|
|
||||||
{usedFormat == null || (usedFormat.single && value == null) ? (
|
|
||||||
<ErrorInfo message="Must be selected one cell" />
|
|
||||||
) : (
|
|
||||||
<Component value={value} grider={grider} selection={selection} />
|
|
||||||
)}
|
|
||||||
</DataWrapper>
|
|
||||||
</MainWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import ReactJson from 'react-json-view';
|
|
||||||
import ErrorInfo from '../widgets/ErrorInfo';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const OuterWrapper = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InnerWrapper = styled.div`
|
|
||||||
overflow: scroll;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function JsonCellView({ value }) {
|
|
||||||
const theme = useTheme();
|
|
||||||
try {
|
|
||||||
const json = React.useMemo(() => JSON.parse(value), [value]);
|
|
||||||
return (
|
|
||||||
<OuterWrapper>
|
|
||||||
<InnerWrapper>
|
|
||||||
<ReactJson src={json} theme={theme.jsonViewerTheme} />
|
|
||||||
</InnerWrapper>
|
|
||||||
</OuterWrapper>
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
return <ErrorInfo message="Error parsing JSON" />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const StyledInput = styled.textarea`
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function TextCellViewWrap({ value, grider, selection }) {
|
|
||||||
return <StyledInput value={value} wrap="hard" readOnly />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TextCellViewNoWrap({ value, grider, selection }) {
|
|
||||||
return (
|
|
||||||
<StyledInput
|
|
||||||
value={value}
|
|
||||||
wrap="off"
|
|
||||||
readOnly
|
|
||||||
// readOnly={grider ? !grider.editable : true}
|
|
||||||
// onChange={(e) => grider.setCellValue(selection[0].row, selection[0].column, e.target.value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Chart from 'react-chartjs-2';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import useDimensions from '../utility/useDimensions';
|
|
||||||
import { HorizontalSplitter } from '../widgets/Splitter';
|
|
||||||
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
|
|
||||||
import { FormCheckboxField, FormSelectField, FormTextField } from '../utility/forms';
|
|
||||||
import DataChart from './DataChart';
|
|
||||||
import { FormProviderCore } from '../utility/FormProvider';
|
|
||||||
import { loadChartData, loadChartStructure } from './chartDataLoader';
|
|
||||||
import useExtensions from '../utility/useExtensions';
|
|
||||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
|
||||||
import { FormFieldTemplateTiny } from '../utility/formStyle';
|
|
||||||
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
|
|
||||||
import { presetPrimaryColors } from '@ant-design/colors';
|
|
||||||
import ErrorInfo from '../widgets/ErrorInfo';
|
|
||||||
|
|
||||||
const LeftContainer = styled.div`
|
|
||||||
background-color: ${props => props.theme.manager_background};
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function ChartEditor({ data, config, setConfig, sql, conid, database }) {
|
|
||||||
const [managerSize, setManagerSize] = React.useState(0);
|
|
||||||
const theme = useTheme();
|
|
||||||
const extensions = useExtensions();
|
|
||||||
const [error, setError] = React.useState(null);
|
|
||||||
|
|
||||||
const [availableColumnNames, setAvailableColumnNames] = React.useState([]);
|
|
||||||
const [loadedData, setLoadedData] = React.useState(null);
|
|
||||||
|
|
||||||
const getDriver = async () => {
|
|
||||||
const conn = await getConnectionInfo({ conid });
|
|
||||||
if (!conn) return;
|
|
||||||
const driver = findEngineDriver(conn, extensions);
|
|
||||||
return driver;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadColumns = async () => {
|
|
||||||
const driver = await getDriver();
|
|
||||||
if (!driver) return;
|
|
||||||
try {
|
|
||||||
const columns = await loadChartStructure(driver, conid, database, sql);
|
|
||||||
setAvailableColumnNames(columns);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadData = async () => {
|
|
||||||
const driver = await getDriver();
|
|
||||||
if (!driver) return;
|
|
||||||
const loaded = await loadChartData(driver, conid, database, sql, config);
|
|
||||||
if (!loaded) return;
|
|
||||||
const { columns, rows } = loaded;
|
|
||||||
setLoadedData({
|
|
||||||
structure: columns,
|
|
||||||
rows,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (sql && conid && database) {
|
|
||||||
handleLoadColumns();
|
|
||||||
}
|
|
||||||
}, [sql, conid, database, extensions]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
setAvailableColumnNames(data ? data.structure.columns.map(x => x.columnName) : []);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (config.labelColumn && sql && conid && database) {
|
|
||||||
handleLoadData();
|
|
||||||
}
|
|
||||||
}, [config, sql, conid, database, availableColumnNames]);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ErrorInfo message={error} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProviderCore values={config} setValues={setConfig} template={FormFieldTemplateTiny}>
|
|
||||||
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
|
|
||||||
<LeftContainer theme={theme}>
|
|
||||||
<WidgetColumnBar>
|
|
||||||
<WidgetColumnBarItem title="Style" name="style" height="40%">
|
|
||||||
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
|
|
||||||
<FormSelectField label="Chart type" name="chartType">
|
|
||||||
<option value="bar">Bar</option>
|
|
||||||
<option value="line">Line</option>
|
|
||||||
{/* <option value="radar">Radar</option> */}
|
|
||||||
<option value="pie">Pie</option>
|
|
||||||
<option value="polarArea">Polar area</option>
|
|
||||||
{/* <option value="bubble">Bubble</option>
|
|
||||||
<option value="scatter">Scatter</option> */}
|
|
||||||
</FormSelectField>
|
|
||||||
<FormTextField label="Color set" name="colorSeed" />
|
|
||||||
<FormSelectField label="Truncate from" name="truncateFrom">
|
|
||||||
<option value="begin">Begin</option>
|
|
||||||
<option value="end">End (most recent data for datetime)</option>
|
|
||||||
</FormSelectField>
|
|
||||||
<FormTextField label="Truncate limit" name="truncateLimit" />
|
|
||||||
<FormCheckboxField label="Show relative values" name="showRelativeValues" />
|
|
||||||
</ManagerInnerContainer>
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
<WidgetColumnBarItem title="Data" name="data">
|
|
||||||
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
|
|
||||||
{availableColumnNames.length > 0 && (
|
|
||||||
<FormSelectField label="Label column" name="labelColumn">
|
|
||||||
<option value=""></option>
|
|
||||||
{availableColumnNames.map(col => (
|
|
||||||
<option value={col} key={col}>
|
|
||||||
{col}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</FormSelectField>
|
|
||||||
)}
|
|
||||||
{availableColumnNames.map(col => (
|
|
||||||
<React.Fragment key={col}>
|
|
||||||
<FormCheckboxField label={col} name={`dataColumn_${col}`} />
|
|
||||||
{config[`dataColumn_${col}`] && (
|
|
||||||
<FormSelectField label="Color" name={`dataColumnColor_${col}`}>
|
|
||||||
<option value="">Random</option>
|
|
||||||
|
|
||||||
{_.keys(presetPrimaryColors).map(color => (
|
|
||||||
<option value={color} key={color}>
|
|
||||||
{_.startCase(color)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</FormSelectField>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</ManagerInnerContainer>
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
</WidgetColumnBar>
|
|
||||||
</LeftContainer>
|
|
||||||
|
|
||||||
<DataChart data={data || loadedData} />
|
|
||||||
</HorizontalSplitter>
|
|
||||||
</FormProviderCore>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ToolbarButton from '../widgets/ToolbarButton';
|
|
||||||
|
|
||||||
export default function ChartToolbar({ modelState, dispatchModel }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
|
|
||||||
Undo
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!modelState.canRedo} onClick={() => dispatchModel({ type: 'redo' })} icon="icon redo">
|
|
||||||
Redo
|
|
||||||
</ToolbarButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import Chart from 'react-chartjs-2';
|
|
||||||
import randomcolor from 'randomcolor';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import useDimensions from '../utility/useDimensions';
|
|
||||||
import { useForm } from '../utility/FormProvider';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const ChartWrapper = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function getTimeAxis(labels) {
|
|
||||||
const res = [];
|
|
||||||
for (const label of labels) {
|
|
||||||
const parsed = moment(label);
|
|
||||||
if (!parsed.isValid()) return null;
|
|
||||||
const iso = parsed.toISOString();
|
|
||||||
if (iso < '1850-01-01T00:00:00' || iso > '2150-01-01T00:00:00') return null;
|
|
||||||
res.push(parsed);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLabels(labelValues, timeAxis, chartType) {
|
|
||||||
if (!timeAxis) return labelValues;
|
|
||||||
if (chartType === 'line') return timeAxis.map(x => x.toDate());
|
|
||||||
return timeAxis.map(x => x.format('D. M. YYYY'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOptions(timeAxis, chartType) {
|
|
||||||
if (timeAxis && chartType === 'line') {
|
|
||||||
return {
|
|
||||||
scales: {
|
|
||||||
xAxes: [
|
|
||||||
{
|
|
||||||
type: 'time',
|
|
||||||
distribution: 'linear',
|
|
||||||
|
|
||||||
time: {
|
|
||||||
tooltipFormat: 'D. M. YYYY HH:mm',
|
|
||||||
displayFormats: {
|
|
||||||
millisecond: 'HH:mm:ss.SSS',
|
|
||||||
second: 'HH:mm:ss',
|
|
||||||
minute: 'HH:mm',
|
|
||||||
hour: 'D.M hA',
|
|
||||||
day: 'D. M.',
|
|
||||||
week: 'D. M. YYYY',
|
|
||||||
month: 'MM-YYYY',
|
|
||||||
quarter: '[Q]Q - YYYY',
|
|
||||||
year: 'YYYY',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createChartData(freeData, labelColumn, dataColumns, colorSeed, chartType, dataColumnColors, theme) {
|
|
||||||
if (!freeData || !labelColumn || !dataColumns || !freeData.rows || dataColumns.length == 0) return [{}, {}];
|
|
||||||
const colors = randomcolor({
|
|
||||||
count: _.max([freeData.rows.length, dataColumns.length, 1]),
|
|
||||||
seed: colorSeed,
|
|
||||||
});
|
|
||||||
let backgroundColor = null;
|
|
||||||
let borderColor = null;
|
|
||||||
const labelValues = freeData.rows.map(x => x[labelColumn]);
|
|
||||||
const timeAxis = getTimeAxis(labelValues);
|
|
||||||
const labels = getLabels(labelValues, timeAxis, chartType);
|
|
||||||
const res = {
|
|
||||||
labels,
|
|
||||||
datasets: dataColumns.map((dataColumn, columnIndex) => {
|
|
||||||
if (chartType == 'line' || chartType == 'bar') {
|
|
||||||
const color = dataColumnColors[dataColumn];
|
|
||||||
if (color) {
|
|
||||||
backgroundColor = theme.main_palettes[color][4] + '80';
|
|
||||||
borderColor = theme.main_palettes[color][7];
|
|
||||||
} else {
|
|
||||||
backgroundColor = colors[columnIndex] + '80';
|
|
||||||
borderColor = colors[columnIndex];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
backgroundColor = colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: dataColumn,
|
|
||||||
data: freeData.rows.map(row => row[dataColumn]),
|
|
||||||
backgroundColor,
|
|
||||||
borderColor,
|
|
||||||
borderWidth: 1,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = getOptions(timeAxis, chartType);
|
|
||||||
return [res, options];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractDataColumns(values) {
|
|
||||||
const dataColumns = [];
|
|
||||||
for (const key in values) {
|
|
||||||
if (key.startsWith('dataColumn_') && values[key]) {
|
|
||||||
dataColumns.push(key.substring('dataColumn_'.length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dataColumns;
|
|
||||||
}
|
|
||||||
export function extractDataColumnColors(values, dataColumns) {
|
|
||||||
const res = {};
|
|
||||||
for (const column of dataColumns) {
|
|
||||||
const color = values[`dataColumnColor_${column}`];
|
|
||||||
if (color) res[column] = color;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DataChart({ data }) {
|
|
||||||
const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions();
|
|
||||||
const { values } = useForm();
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const { labelColumn } = values;
|
|
||||||
const dataColumns = extractDataColumns(values);
|
|
||||||
const dataColumnColors = extractDataColumnColors(values, dataColumns);
|
|
||||||
const [chartData, options] = createChartData(
|
|
||||||
data,
|
|
||||||
labelColumn,
|
|
||||||
dataColumns,
|
|
||||||
values.colorSeed || '5',
|
|
||||||
values.chartType,
|
|
||||||
dataColumnColors,
|
|
||||||
theme
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChartWrapper ref={containerRef}>
|
|
||||||
<Chart
|
|
||||||
key={`${values.chartType}|${containerWidth}|${containerHeight}`}
|
|
||||||
width={containerWidth}
|
|
||||||
height={containerHeight}
|
|
||||||
data={chartData}
|
|
||||||
type={values.chartType}
|
|
||||||
options={{
|
|
||||||
...options,
|
|
||||||
// elements: {
|
|
||||||
// point: {
|
|
||||||
// radius: 0,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// tooltips: {
|
|
||||||
// mode: 'index',
|
|
||||||
// intersect: false,
|
|
||||||
// },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ChartWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { dumpSqlSelect, Select } from 'dbgate-sqltree';
|
|
||||||
import { EngineDriver } from 'dbgate-types';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { extractDataColumns } from './DataChart';
|
|
||||||
|
|
||||||
export async function loadChartStructure(driver: EngineDriver, conid, database, sql) {
|
|
||||||
const select: Select = {
|
|
||||||
commandType: 'select',
|
|
||||||
selectAll: true,
|
|
||||||
topRecords: 1,
|
|
||||||
from: {
|
|
||||||
subQueryString: sql,
|
|
||||||
alias: 'subq',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const dmp = driver.createDumper();
|
|
||||||
dumpSqlSelect(dmp, select);
|
|
||||||
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
|
||||||
if (resp.data.errorMessage) throw new Error(resp.data.errorMessage);
|
|
||||||
return resp.data.columns.map(x => x.columnName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadChartData(driver: EngineDriver, conid, database, sql, config) {
|
|
||||||
const dataColumns = extractDataColumns(config);
|
|
||||||
const { labelColumn, truncateFrom, truncateLimit, showRelativeValues } = config;
|
|
||||||
if (!labelColumn || !dataColumns || dataColumns.length == 0) return null;
|
|
||||||
|
|
||||||
const select: Select = {
|
|
||||||
commandType: 'select',
|
|
||||||
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
exprType: 'column',
|
|
||||||
source: { alias: 'subq' },
|
|
||||||
columnName: labelColumn,
|
|
||||||
alias: labelColumn,
|
|
||||||
},
|
|
||||||
// @ts-ignore
|
|
||||||
...dataColumns.map(columnName => ({
|
|
||||||
exprType: 'call',
|
|
||||||
func: 'SUM',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
exprType: 'column',
|
|
||||||
columnName,
|
|
||||||
source: { alias: 'subq' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
alias: columnName,
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
topRecords: truncateLimit || 100,
|
|
||||||
from: {
|
|
||||||
subQueryString: sql,
|
|
||||||
alias: 'subq',
|
|
||||||
},
|
|
||||||
groupBy: [
|
|
||||||
{
|
|
||||||
exprType: 'column',
|
|
||||||
source: { alias: 'subq' },
|
|
||||||
columnName: labelColumn,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
orderBy: [
|
|
||||||
{
|
|
||||||
exprType: 'column',
|
|
||||||
source: { alias: 'subq' },
|
|
||||||
columnName: labelColumn,
|
|
||||||
direction: truncateFrom == 'end' ? 'DESC' : 'ASC',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const dmp = driver.createDumper();
|
|
||||||
dumpSqlSelect(dmp, select);
|
|
||||||
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
|
||||||
let { rows, columns } = resp.data;
|
|
||||||
if (truncateFrom == 'end' && rows) {
|
|
||||||
rows = _.reverse([...rows]);
|
|
||||||
}
|
|
||||||
if (showRelativeValues) {
|
|
||||||
const maxValues = dataColumns.map(col => _.max(rows.map(row => row[col])));
|
|
||||||
for (const [col, max] of _.zip(dataColumns, maxValues)) {
|
|
||||||
if (!max) continue;
|
|
||||||
if (!_.isNumber(max)) continue;
|
|
||||||
if (!(max > 0)) continue;
|
|
||||||
rows = rows.map(row => ({
|
|
||||||
...row,
|
|
||||||
[col]: (row[col] / max) * 100,
|
|
||||||
}));
|
|
||||||
// columns = columns.map((x) => {
|
|
||||||
// if (x.columnName == col) {
|
|
||||||
// return { columnName: `${col} %` };
|
|
||||||
// }
|
|
||||||
// return x;
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeSet,
|
|
||||||
changeSetContainsChanges,
|
|
||||||
changeSetInsertNewRow,
|
|
||||||
createChangeSet,
|
|
||||||
deleteChangeSetRows,
|
|
||||||
findExistingChangeSetItem,
|
|
||||||
getChangeSetInsertedRows,
|
|
||||||
GridDisplay,
|
|
||||||
revertChangeSetRowChanges,
|
|
||||||
setChangeSetValue,
|
|
||||||
} from 'dbgate-datalib';
|
|
||||||
import Grider, { GriderRowStatus } from './Grider';
|
|
||||||
|
|
||||||
export default class ChangeSetGrider extends Grider {
|
|
||||||
public insertedRows: any[];
|
|
||||||
public changeSet: ChangeSet;
|
|
||||||
public setChangeSet: Function;
|
|
||||||
private rowCacheIndexes: Set<number>;
|
|
||||||
private rowDataCache;
|
|
||||||
private rowStatusCache;
|
|
||||||
private rowDefinitionsCache;
|
|
||||||
private batchChangeSet: ChangeSet;
|
|
||||||
|
|
||||||
constructor(public sourceRows: any[], public changeSetState, public dispatchChangeSet, public display: GridDisplay) {
|
|
||||||
super();
|
|
||||||
this.changeSet = changeSetState && changeSetState.value;
|
|
||||||
this.insertedRows = getChangeSetInsertedRows(this.changeSet, display.baseTable);
|
|
||||||
this.setChangeSet = value => dispatchChangeSet({ type: 'set', value });
|
|
||||||
this.rowCacheIndexes = new Set();
|
|
||||||
this.rowDataCache = {};
|
|
||||||
this.rowStatusCache = {};
|
|
||||||
this.rowDefinitionsCache = {};
|
|
||||||
this.batchChangeSet = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRowSource(index: number) {
|
|
||||||
if (index < this.sourceRows.length) return this.sourceRows[index];
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getInsertedRowIndex(index) {
|
|
||||||
return index >= this.sourceRows.length ? index - this.sourceRows.length : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
requireRowCache(index: number) {
|
|
||||||
if (this.rowCacheIndexes.has(index)) return;
|
|
||||||
const row = this.getRowSource(index);
|
|
||||||
const insertedRowIndex = this.getInsertedRowIndex(index);
|
|
||||||
const rowDefinition = this.display.getChangeSetRow(row, insertedRowIndex);
|
|
||||||
const [matchedField, matchedChangeSetItem] = findExistingChangeSetItem(this.changeSet, rowDefinition);
|
|
||||||
const rowUpdated = matchedChangeSetItem ? { ...row, ...matchedChangeSetItem.fields } : row;
|
|
||||||
let status = 'regular';
|
|
||||||
if (matchedChangeSetItem && matchedField == 'updates') status = 'updated';
|
|
||||||
if (matchedField == 'deletes') status = 'deleted';
|
|
||||||
if (insertedRowIndex != null) status = 'inserted';
|
|
||||||
const rowStatus = {
|
|
||||||
status,
|
|
||||||
modifiedFields:
|
|
||||||
matchedChangeSetItem && matchedChangeSetItem.fields ? new Set(Object.keys(matchedChangeSetItem.fields)) : null,
|
|
||||||
};
|
|
||||||
this.rowDataCache[index] = rowUpdated;
|
|
||||||
this.rowStatusCache[index] = rowStatus;
|
|
||||||
this.rowDefinitionsCache[index] = rowDefinition;
|
|
||||||
this.rowCacheIndexes.add(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
get editable() {
|
|
||||||
return this.display.editable;
|
|
||||||
}
|
|
||||||
|
|
||||||
get canInsert() {
|
|
||||||
return !!this.display.baseTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRowData(index: number) {
|
|
||||||
this.requireRowCache(index);
|
|
||||||
return this.rowDataCache[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
getRowStatus(index): GriderRowStatus {
|
|
||||||
this.requireRowCache(index);
|
|
||||||
return this.rowStatusCache[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
get rowCount() {
|
|
||||||
return this.sourceRows.length + this.insertedRows.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyModification(changeSetReducer) {
|
|
||||||
if (this.batchChangeSet) {
|
|
||||||
this.batchChangeSet = changeSetReducer(this.batchChangeSet);
|
|
||||||
} else {
|
|
||||||
this.setChangeSet(changeSetReducer(this.changeSet));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCellValue(index: number, uniqueName: string, value: any) {
|
|
||||||
const row = this.getRowSource(index);
|
|
||||||
const definition = this.display.getChangeSetField(row, uniqueName, this.getInsertedRowIndex(index));
|
|
||||||
this.applyModification(chs => setChangeSetValue(chs, definition, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteRow(index: number) {
|
|
||||||
this.requireRowCache(index);
|
|
||||||
this.applyModification(chs => deleteChangeSetRows(chs, this.rowDefinitionsCache[index]));
|
|
||||||
}
|
|
||||||
|
|
||||||
get rowCountInUpdate() {
|
|
||||||
if (this.batchChangeSet) {
|
|
||||||
const newRows = getChangeSetInsertedRows(this.batchChangeSet, this.display.baseTable);
|
|
||||||
return this.sourceRows.length + newRows.length;
|
|
||||||
} else {
|
|
||||||
return this.rowCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
insertRow(): number {
|
|
||||||
const res = this.rowCountInUpdate;
|
|
||||||
this.applyModification(chs => changeSetInsertNewRow(chs, this.display.baseTable));
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
beginUpdate() {
|
|
||||||
this.batchChangeSet = this.changeSet;
|
|
||||||
}
|
|
||||||
endUpdate() {
|
|
||||||
this.setChangeSet(this.batchChangeSet);
|
|
||||||
this.batchChangeSet = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
revertRowChanges(index: number) {
|
|
||||||
this.requireRowCache(index);
|
|
||||||
this.applyModification(chs => revertChangeSetRowChanges(chs, this.rowDefinitionsCache[index]));
|
|
||||||
}
|
|
||||||
revertAllChanges() {
|
|
||||||
this.applyModification(chs => createChangeSet());
|
|
||||||
}
|
|
||||||
undo() {
|
|
||||||
this.dispatchChangeSet({ type: 'undo' });
|
|
||||||
}
|
|
||||||
redo() {
|
|
||||||
this.dispatchChangeSet({ type: 'redo' });
|
|
||||||
}
|
|
||||||
get canUndo() {
|
|
||||||
return this.changeSetState.canUndo;
|
|
||||||
}
|
|
||||||
get canRedo() {
|
|
||||||
return this.changeSetState.canRedo;
|
|
||||||
}
|
|
||||||
get containsChanges() {
|
|
||||||
return changeSetContainsChanges(this.changeSet);
|
|
||||||
}
|
|
||||||
get disableLoadNextPage() {
|
|
||||||
return this.insertedRows.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
|
||||||
return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
|
||||||
}
|
|
||||||
static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
|
||||||
return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import ColumnLabel from './ColumnLabel';
|
|
||||||
import DropDownButton from '../widgets/DropDownButton';
|
|
||||||
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
|
||||||
import { useSplitterDrag } from '../widgets/Splitter';
|
|
||||||
import { isTypeDateTime } from 'dbgate-tools';
|
|
||||||
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject';
|
|
||||||
import { useSetOpenedTabs } from '../utility/globalState';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
|
|
||||||
const HeaderDiv = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LabelDiv = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
min-width: 10px;
|
|
||||||
// padding-left: 2px;
|
|
||||||
padding: 2px;
|
|
||||||
margin: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const IconWrapper = styled.span`
|
|
||||||
margin-left: 3px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ResizeHandle = styled.div`
|
|
||||||
background-color: ${props => props.theme.border};
|
|
||||||
width: 2px;
|
|
||||||
cursor: col-resize;
|
|
||||||
z-index: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GroupingLabel = styled.span`
|
|
||||||
color: green;
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function ColumnHeaderControl({
|
|
||||||
column,
|
|
||||||
setSort,
|
|
||||||
onResize,
|
|
||||||
order,
|
|
||||||
setGrouping,
|
|
||||||
grouping,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
}) {
|
|
||||||
const onResizeDown = useSplitterDrag('clientX', onResize);
|
|
||||||
const { foreignKey } = column;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const openReferencedTable = () => {
|
|
||||||
openDatabaseObjectDetail(openNewTab, 'TableDataTab', null, {
|
|
||||||
schemaName: foreignKey.refSchemaName,
|
|
||||||
pureName: foreignKey.refTableName,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
objectTypeField: 'tables',
|
|
||||||
});
|
|
||||||
// openNewTab(setOpenedTabs, {
|
|
||||||
// title: foreignKey.refTableName,
|
|
||||||
// tooltip,
|
|
||||||
// icon: sqlTemplate ? 'sql.svg' : icons[objectTypeField],
|
|
||||||
// tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
|
|
||||||
// props: {
|
|
||||||
// schemaName,
|
|
||||||
// pureName,
|
|
||||||
// conid,
|
|
||||||
// database,
|
|
||||||
// objectTypeField,
|
|
||||||
// initialArgs: sqlTemplate ? { sqlTemplate } : null,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<HeaderDiv>
|
|
||||||
<LabelDiv>
|
|
||||||
{grouping && (
|
|
||||||
<GroupingLabel>{grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}:</GroupingLabel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ColumnLabel {...column} />
|
|
||||||
{order == 'ASC' && (
|
|
||||||
<IconWrapper>
|
|
||||||
<FontIcon icon="img sort-asc" />
|
|
||||||
</IconWrapper>
|
|
||||||
)}
|
|
||||||
{order == 'DESC' && (
|
|
||||||
<IconWrapper>
|
|
||||||
<FontIcon icon="img sort-desc" />
|
|
||||||
</IconWrapper>
|
|
||||||
)}
|
|
||||||
</LabelDiv>
|
|
||||||
{setSort && (
|
|
||||||
<DropDownButton>
|
|
||||||
<DropDownMenuItem onClick={() => setSort('ASC')}>Sort ascending</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setSort('DESC')}>Sort descending</DropDownMenuItem>
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
{foreignKey && (
|
|
||||||
<DropDownMenuItem onClick={openReferencedTable}>
|
|
||||||
Open table <strong>{foreignKey.refTableName}</strong>
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{foreignKey && <DropDownMenuDivider />}
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP')}>Group by</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('MAX')}>MAX</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('MIN')}>MIN</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('SUM')}>SUM</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('AVG')}>AVG</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('COUNT')}>COUNT</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('COUNT DISTINCT')}>COUNT DISTINCT</DropDownMenuItem>
|
|
||||||
{isTypeDateTime(column.dataType) && (
|
|
||||||
<>
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:YEAR')}>Group by YEAR</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:MONTH')}>Group by MONTH</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:DAY')}>Group by DAY</DropDownMenuItem>
|
|
||||||
{/* <DropDownMenuItem onClick={() => setGrouping('GROUP:HOUR')}>Group by HOUR</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:MINUTE')}>Group by MINUTE</DropDownMenuItem> */}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropDownButton>
|
|
||||||
)}
|
|
||||||
<ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} theme={theme} />
|
|
||||||
</HeaderDiv>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
//@ts-nocheck
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const Label = styled.span`
|
|
||||||
font-weight: ${props => (props.notNull ? 'bold' : 'normal')};
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
const ExtInfoWrap = styled.span`
|
|
||||||
font-weight: normal;
|
|
||||||
margin-left: 5px;
|
|
||||||
color: ${props => props.theme.left_font3};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function getColumnIcon(column, forceIcon = false) {
|
|
||||||
if (column.autoIncrement) return 'img autoincrement';
|
|
||||||
if (column.foreignKey) return 'img foreign-key';
|
|
||||||
if (forceIcon) return 'img column';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param column {import('dbgate-datalib').DisplayColumn|import('dbgate-types').ColumnInfo} */
|
|
||||||
export default function ColumnLabel(column) {
|
|
||||||
const icon = getColumnIcon(column, column.forceIcon);
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<Label {...column}>
|
|
||||||
{icon ? <FontIcon icon={icon} /> : null} {column.headerText || column.columnName}
|
|
||||||
{column.extInfo ? <ExtInfoWrap theme={theme}>{column.extInfo}</ExtInfoWrap> : null}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import ColumnLabel from './ColumnLabel';
|
|
||||||
import { filterName } from 'dbgate-datalib';
|
|
||||||
import { ExpandIcon } from '../icons';
|
|
||||||
import InlineButton from '../widgets/InlineButton';
|
|
||||||
import { ManagerInnerContainer } from './ManagerStyles';
|
|
||||||
import SearchInput from '../widgets/SearchInput';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const Wrapper = styled.div``;
|
|
||||||
|
|
||||||
const Row = styled.div`
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
&:hover {
|
|
||||||
background-color: ${props => props.theme.manager_background_blue[1]};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SearchBoxWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Button = styled.button`
|
|
||||||
// -webkit-appearance: none;
|
|
||||||
// -moz-appearance: none;
|
|
||||||
// appearance: none;
|
|
||||||
// width: 50px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {object} props
|
|
||||||
* @param {import('dbgate-datalib').GridDisplay} props.display
|
|
||||||
* @param {import('dbgate-datalib').DisplayColumn} props.column
|
|
||||||
*/
|
|
||||||
function ColumnManagerRow(props) {
|
|
||||||
const { display, column } = props;
|
|
||||||
const [isHover, setIsHover] = React.useState(false);
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<Row
|
|
||||||
onMouseEnter={() => setIsHover(true)}
|
|
||||||
onMouseLeave={() => setIsHover(false)}
|
|
||||||
theme={theme}
|
|
||||||
onClick={e => {
|
|
||||||
// @ts-ignore
|
|
||||||
if (e.target.closest('.expandColumnIcon')) return;
|
|
||||||
display.focusColumn(column.uniqueName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExpandIcon
|
|
||||||
className="expandColumnIcon"
|
|
||||||
isBlank={!column.foreignKey}
|
|
||||||
isExpanded={column.foreignKey && display.isExpandedColumn(column.uniqueName)}
|
|
||||||
onClick={() => display.toggleExpandedColumn(column.uniqueName)}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
style={{ marginLeft: `${5 + (column.uniquePath.length - 1) * 10}px` }}
|
|
||||||
checked={column.isChecked}
|
|
||||||
onChange={() => display.setColumnVisibility(column.uniquePath, !column.isChecked)}
|
|
||||||
></input>
|
|
||||||
<ColumnLabel {...column} />
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param props {import('./types').DataGridProps} */
|
|
||||||
export default function ColumnManager(props) {
|
|
||||||
const { display } = props;
|
|
||||||
const [columnFilter, setColumnFilter] = React.useState('');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SearchBoxWrapper>
|
|
||||||
<SearchInput placeholder="Search columns" filter={columnFilter} setFilter={setColumnFilter} />
|
|
||||||
<InlineButton onClick={() => display.hideAllColumns()}>Hide</InlineButton>
|
|
||||||
<InlineButton onClick={() => display.showAllColumns()}>Show</InlineButton>
|
|
||||||
</SearchBoxWrapper>
|
|
||||||
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
|
|
||||||
{display
|
|
||||||
.getColumns(columnFilter)
|
|
||||||
.filter(column => filterName(columnFilter, column.columnName))
|
|
||||||
.map(column => (
|
|
||||||
<ColumnManagerRow key={column.uniqueName} display={display} column={column} />
|
|
||||||
))}
|
|
||||||
</ManagerInnerContainer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,501 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import keycodes from '../utility/keycodes';
|
|
||||||
import { parseFilter, createMultiLineFilter } from 'dbgate-filterparser';
|
|
||||||
import InlineButton from '../widgets/InlineButton';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import FilterMultipleValuesModal from '../modals/FilterMultipleValuesModal';
|
|
||||||
import SetFilterModal from '../modals/SetFilterModal';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import { useShowMenu } from '../modals/showMenu';
|
|
||||||
// import { $ } from '../../Utility/jquery';
|
|
||||||
// import autobind from 'autobind-decorator';
|
|
||||||
// import * as React from 'react';
|
|
||||||
|
|
||||||
// import { createMultiLineFilter } from '../../DataLib/FilterTools';
|
|
||||||
// import { ModalDialog } from '../Dialogs';
|
|
||||||
// import { FilterDialog } from '../Dialogs/FilterDialog';
|
|
||||||
// import { FilterMultipleValuesDialog } from '../Dialogs/FilterMultipleValuesDialog';
|
|
||||||
// import { IconSpan } from '../Navigation/NavUtils';
|
|
||||||
// import { KeyCodes } from '../ReactDataGrid/KeyCodes';
|
|
||||||
// import { DropDownMenu, DropDownMenuDivider, DropDownMenuItem, DropDownSubmenuItem } from './DropDownMenu';
|
|
||||||
// import { FilterParserType } from '../../SwaggerClients';
|
|
||||||
// import { IFilterHolder } from '../CommonControls';
|
|
||||||
// import { GrayFilterIcon } from '../Icons';
|
|
||||||
|
|
||||||
// export interface IDataFilterControlProps {
|
|
||||||
// filterType: FilterParserType;
|
|
||||||
// getFilter: Function;
|
|
||||||
// setFilter: Function;
|
|
||||||
// width: number;
|
|
||||||
// onControlKey?: Function;
|
|
||||||
// isReadOnly?: boolean;
|
|
||||||
// inputElementId?: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const FilterDiv = styled.div`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
const FilterInput = styled.input`
|
|
||||||
flex: 1;
|
|
||||||
min-width: 10px;
|
|
||||||
background-color: ${props =>
|
|
||||||
props.state == 'ok'
|
|
||||||
? props.theme.input_background_green[1]
|
|
||||||
: props.state == 'error'
|
|
||||||
? props.theme.input_background_red[1]
|
|
||||||
: props.theme.input_background};
|
|
||||||
`;
|
|
||||||
// const FilterButton = styled.button`
|
|
||||||
// color: gray;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
function DropDownContent({ filterType, setFilter, filterMultipleValues, openFilterWindow }) {
|
|
||||||
switch (filterType) {
|
|
||||||
case 'number':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('=')}>Equals...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('>')}>Greater Than...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('>=')}>Greater Than Or Equal To...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('<')}>Less Than...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('<=')}>Less Than Or Equal To...</DropDownMenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case 'logical':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('TRUE')}>Is True</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('FALSE')}>Is False</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('TRUE, NULL')}>Is True or NULL</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('FALSE, NULL')}>Is False or NULL</DropDownMenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case 'datetime':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
|
||||||
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('<=')}>Before...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('>=')}>After...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('>=;<=')}>Between...</DropDownMenuItem>
|
|
||||||
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('TOMORROW')}>Tomorrow</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('TODAY')}>Today</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('YESTERDAY')}>Yesterday</DropDownMenuItem>
|
|
||||||
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NEXT WEEK')}>Next Week</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('THIS WEEK')}>This Week</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('LAST WEEK')}>Last Week</DropDownMenuItem>
|
|
||||||
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NEXT MONTH')}>Next Month</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('THIS MONTH')}>This Month</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('LAST MONTH')}>Last Month</DropDownMenuItem>
|
|
||||||
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NEXT YEAR')}>Next Year</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('THIS YEAR')}>This Year</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('LAST YEAR')}>Last Year</DropDownMenuItem>
|
|
||||||
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
|
|
||||||
{/* <DropDownSubmenuItem title="All dates in period">
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('JAN')}>January</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('FEB')}>February</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('MAR')}>March</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('APR')}>April</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('JUN')}>June</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('JUL')}>July</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('AUG')}>August</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('SEP')}>September</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('OCT')}>October</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NOV')}>November</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('DEC')}>December</DropDownMenuItem>
|
|
||||||
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('MON')}>Monday</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('TUE')}>Tuesday</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('WED')}>Wednesday</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('THU')}>Thursday</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('FRI')}>Friday</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('SAT')}>Saturday</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('SUN')}>Sunday</DropDownMenuItem>
|
|
||||||
</DropDownSubmenuItem> */}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case 'string':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
|
||||||
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('=')}>Equals...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('EMPTY, NULL')}>Is Empty Or Null</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value</DropDownMenuItem>
|
|
||||||
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('+')}>Contains...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('~')}>Does Not Contain...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('^')}>Begins With...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('!^')}>Does Not Begin With...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('$')}>Ends With...</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={x => openFilterWindow('!$')}>Does Not End With...</DropDownMenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DataFilterControl({
|
|
||||||
isReadOnly = false,
|
|
||||||
filterType,
|
|
||||||
filter,
|
|
||||||
setFilter,
|
|
||||||
focusIndex = 0,
|
|
||||||
onFocusGrid = undefined,
|
|
||||||
}) {
|
|
||||||
const showModal = useShowModal();
|
|
||||||
const showMenu = useShowMenu();
|
|
||||||
const theme = useTheme();
|
|
||||||
const [filterState, setFilterState] = React.useState('empty');
|
|
||||||
const setFilterText = filter => {
|
|
||||||
setFilter(filter);
|
|
||||||
editorRef.current.value = filter || '';
|
|
||||||
updateFilterState();
|
|
||||||
};
|
|
||||||
const applyFilter = () => {
|
|
||||||
if ((filter || '') == (editorRef.current.value || '')) return;
|
|
||||||
setFilter(editorRef.current.value);
|
|
||||||
};
|
|
||||||
const filterMultipleValues = () => {
|
|
||||||
showModal(modalState => (
|
|
||||||
<FilterMultipleValuesModal
|
|
||||||
modalState={modalState}
|
|
||||||
onFilter={(mode, text) => setFilterText(createMultiLineFilter(mode, text))}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
const openFilterWindow = operator => {
|
|
||||||
showModal(modalState => (
|
|
||||||
<SetFilterModal
|
|
||||||
filterType={filterType}
|
|
||||||
modalState={modalState}
|
|
||||||
onFilter={text => setFilterText(text)}
|
|
||||||
condition1={operator}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
const buttonRef = React.useRef();
|
|
||||||
const editorRef = React.useRef();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (focusIndex) editorRef.current.focus();
|
|
||||||
}, [focusIndex]);
|
|
||||||
|
|
||||||
const handleKeyDown = ev => {
|
|
||||||
if (isReadOnly) return;
|
|
||||||
if (ev.keyCode == keycodes.enter) {
|
|
||||||
applyFilter();
|
|
||||||
}
|
|
||||||
if (ev.keyCode == keycodes.escape) {
|
|
||||||
setFilterText('');
|
|
||||||
}
|
|
||||||
if (ev.keyCode == keycodes.downArrow) {
|
|
||||||
if (onFocusGrid) onFocusGrid();
|
|
||||||
// ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
|
|
||||||
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFilterState = () => {
|
|
||||||
const value = editorRef.current.value;
|
|
||||||
try {
|
|
||||||
if (value) {
|
|
||||||
parseFilter(value, filterType);
|
|
||||||
setFilterState('ok');
|
|
||||||
} else {
|
|
||||||
setFilterState('empty');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// console.log('PARSE ERROR', err);
|
|
||||||
setFilterState('error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
editorRef.current.value = filter || '';
|
|
||||||
updateFilterState();
|
|
||||||
}, [filter]);
|
|
||||||
|
|
||||||
const handleShowMenu = () => {
|
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
|
||||||
showMenu(
|
|
||||||
rect.left,
|
|
||||||
rect.bottom,
|
|
||||||
<DropDownContent
|
|
||||||
filterType={filterType}
|
|
||||||
setFilter={setFilterText}
|
|
||||||
filterMultipleValues={filterMultipleValues}
|
|
||||||
openFilterWindow={openFilterWindow}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function handlePaste(event) {
|
|
||||||
var pastedText = undefined;
|
|
||||||
// @ts-ignore
|
|
||||||
if (window.clipboardData && window.clipboardData.getData) {
|
|
||||||
// IE
|
|
||||||
// @ts-ignore
|
|
||||||
pastedText = window.clipboardData.getData('Text');
|
|
||||||
} else if (event.clipboardData && event.clipboardData.getData) {
|
|
||||||
pastedText = event.clipboardData.getData('text/plain');
|
|
||||||
}
|
|
||||||
if (pastedText && pastedText.includes('\n')) {
|
|
||||||
event.preventDefault();
|
|
||||||
setFilterText(createMultiLineFilter('is', pastedText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterDiv>
|
|
||||||
<FilterInput
|
|
||||||
theme={theme}
|
|
||||||
ref={editorRef}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
type="text"
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
onChange={updateFilterState}
|
|
||||||
state={filterState}
|
|
||||||
onBlur={applyFilter}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
<InlineButton buttonRef={buttonRef} onClick={handleShowMenu} square>
|
|
||||||
<FontIcon icon="icon filter" />
|
|
||||||
</InlineButton>
|
|
||||||
</FilterDiv>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// domEditor: Element;
|
|
||||||
|
|
||||||
// @autobind
|
|
||||||
// applyFilter() {
|
|
||||||
// this.props.setFilter($(this.domEditor).val());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @autobind
|
|
||||||
// clearFilter() {
|
|
||||||
// $(this.domEditor).val('');
|
|
||||||
// this.applyFilter();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setFilter(value: string) {
|
|
||||||
// $(this.domEditor).val(value);
|
|
||||||
// this.applyFilter();
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// render() {
|
|
||||||
// let dropDownContent = null;
|
|
||||||
|
|
||||||
// let filterIconSpan = <span className='fa fa-filter' style={{color: 'gray', display: 'inline-block', width: '8px', height: '0', whiteSpace: 'nowrap'}} ></span>;
|
|
||||||
// //filterIconSpan = null;
|
|
||||||
|
|
||||||
// if (this.props.filterType == 'Number') {
|
|
||||||
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('=')}>Equals...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('>')}>Greater Than...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=')}>Greater Than Or Equal To...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<')}>Less Than...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<=')}>Less Than Or Equal To...</DropDownMenuItem>
|
|
||||||
// </DropDownMenu>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (this.props.filterType == 'Logical') {
|
|
||||||
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('TRUE')}>Is True</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('FALSE')}>Is False</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('TRUE, NULL')}>Is True or NULL</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('FALSE, NULL')}>Is False or NULL</DropDownMenuItem>
|
|
||||||
// </DropDownMenu>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (this.props.filterType == 'DateTime') {
|
|
||||||
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
|
||||||
|
|
||||||
// <DropDownMenuDivider />
|
|
||||||
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<=')}>Before...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=')}>After...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=;<=')}>Between...</DropDownMenuItem>
|
|
||||||
|
|
||||||
// <DropDownMenuDivider />
|
|
||||||
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('TOMORROW')}>Tomorrow</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('TODAY')}>Today</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('YESTERDAY')}>Yesterday</DropDownMenuItem>
|
|
||||||
|
|
||||||
// <DropDownMenuDivider />
|
|
||||||
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NEXT WEEK')}>Next Week</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('THIS WEEK')}>This Week</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('LAST WEEK')}>Last Week</DropDownMenuItem>
|
|
||||||
|
|
||||||
// <DropDownMenuDivider />
|
|
||||||
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NEXT MONTH')}>Next Month</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('THIS MONTH')}>This Month</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('LAST MONTH')}>Last Month</DropDownMenuItem>
|
|
||||||
|
|
||||||
// <DropDownMenuDivider />
|
|
||||||
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NEXT YEAR')}>Next Year</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('THIS YEAR')}>This Year</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('LAST YEAR')}>Last Year</DropDownMenuItem>
|
|
||||||
|
|
||||||
// <DropDownMenuDivider />
|
|
||||||
|
|
||||||
// <DropDownSubmenuItem title='All dates in period'>
|
|
||||||
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('JAN')}>January</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('FEB')}>February</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('MAR')}>March</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('APR')}>April</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('JUN')}>June</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('JUL')}>July</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('AUG')}>August</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('SEP')}>September</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('OCT')}>October</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NOV')}>November</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('DEC')}>December</DropDownMenuItem>
|
|
||||||
|
|
||||||
// <DropDownMenuDivider />
|
|
||||||
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('MON')}>Monday</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('TUE')}>Tuesday</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('WED')}>Wednesday</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('THU')}>Thursday</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('FRI')}>Friday</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('SAT')}>Saturday</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('SUN')}>Sunday</DropDownMenuItem>
|
|
||||||
|
|
||||||
// </DropDownSubmenuItem>
|
|
||||||
// </DropDownMenu>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (this.props.filterType == 'String') {
|
|
||||||
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
|
||||||
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('=')}>Equals...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('EMPTY, NULL')}>Is Empty Or Null</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value</DropDownMenuItem>
|
|
||||||
|
|
||||||
// <DropDownMenuDivider />
|
|
||||||
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('+')}>Contains...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('~')}>Does Not Contain...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('^')}>Begins With...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('!^')}>Does Not Begin With...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('$')}>Ends With...</DropDownMenuItem>
|
|
||||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('!$')}>Does Not End With...</DropDownMenuItem>
|
|
||||||
// </DropDownMenu>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (this.props.isReadOnly) {
|
|
||||||
// dropDownContent = <GrayFilterIcon style={{marginLeft: '5px'}} />;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return <div style={{ minWidth: `${this.props.width}px`, maxWidth: `${this.props.width}px`, width: `${this.props.width}` }}>
|
|
||||||
// <input id={this.props.inputElementId} type='text' style={{ 'width': `${(this.props.width - 20)}px` }} readOnly={this.props.isReadOnly}
|
|
||||||
// onBlur={this.applyFilter} ref={x => this.setDomEditor(x)} onKeyDown={this.editorKeyDown} placeholder='Search' ></input>
|
|
||||||
|
|
||||||
// {dropDownContent}
|
|
||||||
// </div>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async filterMultipleValues() {
|
|
||||||
// let result = await ModalDialog.run(<FilterMultipleValuesDialog header='Filter multiple values' />);
|
|
||||||
// if (!result) return;
|
|
||||||
// let { mode, text } = result;
|
|
||||||
// let filter = createMultiLineFilter(mode, text);
|
|
||||||
// this.setFilter(filter);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// openFilterWindow(selectedOperator: string) {
|
|
||||||
// FilterDialog.runFilter(this, this.props.filterType, selectedOperator);
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setDomEditor(editor) {
|
|
||||||
// this.domEditor = editor;
|
|
||||||
// $(editor).val(this.props.getFilter());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @autobind
|
|
||||||
// editorKeyDown(ev) {
|
|
||||||
// if (this.props.isReadOnly) return;
|
|
||||||
// if (ev.keyCode == KeyCodes.Enter) {
|
|
||||||
// this.applyFilter();
|
|
||||||
// }
|
|
||||||
// if (ev.keyCode == KeyCodes.Escape) {
|
|
||||||
// this.clearFilter();
|
|
||||||
// }
|
|
||||||
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
|
|
||||||
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// focus() {
|
|
||||||
// $(this.domEditor).focus();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import ColumnManager from './ColumnManager';
|
|
||||||
import FormViewFilters from '../formview/FormViewFilters';
|
|
||||||
|
|
||||||
import ReferenceManager from './ReferenceManager';
|
|
||||||
import { HorizontalSplitter } from '../widgets/Splitter';
|
|
||||||
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
|
|
||||||
import CellDataView from '../celldata/CellDataView';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const LeftContainer = styled.div`
|
|
||||||
background-color: ${props => props.theme.manager_background};
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DataGridContainer = styled.div`
|
|
||||||
position: relative;
|
|
||||||
flex-grow: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function DataGrid(props) {
|
|
||||||
const { GridCore, FormView, formDisplay } = props;
|
|
||||||
const theme = useTheme();
|
|
||||||
const [managerSize, setManagerSize] = React.useState(0);
|
|
||||||
const [selection, setSelection] = React.useState([]);
|
|
||||||
const [formSelection, setFormSelection] = React.useState(null);
|
|
||||||
const [grider, setGrider] = React.useState(null);
|
|
||||||
const [collapsedWidgets, setCollapsedWidgets] = React.useState([]);
|
|
||||||
// const [formViewData, setFormViewData] = React.useState(null);
|
|
||||||
const isFormView = !!(formDisplay && formDisplay.config && formDisplay.config.isFormView);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
|
|
||||||
<LeftContainer theme={theme}>
|
|
||||||
<WidgetColumnBar onChangeCollapsedWidgets={setCollapsedWidgets}>
|
|
||||||
{!isFormView && (
|
|
||||||
<WidgetColumnBarItem title="Columns" name="columns" height={props.showReferences ? '40%' : '60%'}>
|
|
||||||
<ColumnManager {...props} managerSize={managerSize} />
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
)}
|
|
||||||
{isFormView && (
|
|
||||||
<WidgetColumnBarItem title="Filters" name="filters" height="30%">
|
|
||||||
<FormViewFilters {...props} managerSize={managerSize} />
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
)}
|
|
||||||
{props.showReferences && props.display.hasReferences && (
|
|
||||||
<WidgetColumnBarItem title="References" name="references" height="30%" collapsed={props.isDetailView}>
|
|
||||||
<ReferenceManager {...props} managerSize={managerSize} />
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
)}
|
|
||||||
<WidgetColumnBarItem
|
|
||||||
title="Cell data"
|
|
||||||
name="cellData"
|
|
||||||
// cell data must be collapsed by default, because of performance reasons
|
|
||||||
// when not collapsed, onSelectionChanged of grid is set and RERENDER of this component is done on every selection change
|
|
||||||
collapsed
|
|
||||||
>
|
|
||||||
{isFormView ? (
|
|
||||||
<CellDataView selectedValue={formSelection} />
|
|
||||||
) : (
|
|
||||||
<CellDataView selection={selection} grider={grider} />
|
|
||||||
)}
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
</WidgetColumnBar>
|
|
||||||
</LeftContainer>
|
|
||||||
|
|
||||||
<DataGridContainer>
|
|
||||||
{isFormView ? (
|
|
||||||
<FormView {...props} onSelectionChanged={collapsedWidgets.includes('cellData') ? null : setFormSelection} />
|
|
||||||
) : (
|
|
||||||
<GridCore
|
|
||||||
{...props}
|
|
||||||
onSelectionChanged={collapsedWidgets.includes('cellData') ? null : setSelection}
|
|
||||||
onChangeGrider={setGrider}
|
|
||||||
formViewAvailable={!!FormView && !!formDisplay}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DataGridContainer>
|
|
||||||
</HorizontalSplitter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
|
||||||
|
|
||||||
export default function DataGridContextMenu({
|
|
||||||
copy,
|
|
||||||
revertRowChanges,
|
|
||||||
deleteSelectedRows,
|
|
||||||
insertNewRow,
|
|
||||||
setNull,
|
|
||||||
reload,
|
|
||||||
exportGrid,
|
|
||||||
filterSelectedValue,
|
|
||||||
openQuery,
|
|
||||||
openFreeTable,
|
|
||||||
openChartSelection,
|
|
||||||
openActiveChart,
|
|
||||||
switchToForm,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!!reload && (
|
|
||||||
<DropDownMenuItem onClick={reload} keyText="F5">
|
|
||||||
Reload
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{!!reload && <DropDownMenuDivider />}
|
|
||||||
<DropDownMenuItem onClick={copy} keyText="Ctrl+C">
|
|
||||||
Copy
|
|
||||||
</DropDownMenuItem>
|
|
||||||
{revertRowChanges && (
|
|
||||||
<DropDownMenuItem onClick={revertRowChanges} keyText="Ctrl+R">
|
|
||||||
Revert row changes
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{deleteSelectedRows && (
|
|
||||||
<DropDownMenuItem onClick={deleteSelectedRows} keyText="Ctrl+Delete">
|
|
||||||
Delete selected rows
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{insertNewRow && (
|
|
||||||
<DropDownMenuItem onClick={insertNewRow} keyText="Insert">
|
|
||||||
Insert new row
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
{setNull && (
|
|
||||||
<DropDownMenuItem onClick={setNull} keyText="Ctrl+0">
|
|
||||||
Set NULL
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{exportGrid && <DropDownMenuItem onClick={exportGrid}>Export</DropDownMenuItem>}
|
|
||||||
{filterSelectedValue && (
|
|
||||||
<DropDownMenuItem onClick={filterSelectedValue} keyText="Ctrl+F">
|
|
||||||
Filter selected value
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{openQuery && <DropDownMenuItem onClick={openQuery}>Open query</DropDownMenuItem>}
|
|
||||||
<DropDownMenuItem onClick={openFreeTable}>Open selection in free table editor</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={openChartSelection}>Open chart from selection</DropDownMenuItem>
|
|
||||||
{openActiveChart && <DropDownMenuItem onClick={openActiveChart}>Open active chart</DropDownMenuItem>}
|
|
||||||
{!!switchToForm && (
|
|
||||||
<DropDownMenuItem onClick={switchToForm} keyText="F4">
|
|
||||||
Form view
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,333 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import moment from 'moment';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import InplaceEditor from './InplaceEditor';
|
|
||||||
import { cellIsSelected } from './gridutil';
|
|
||||||
import { isTypeLogical } from 'dbgate-tools';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
|
|
||||||
const TableBodyCell = styled.td`
|
|
||||||
font-weight: normal;
|
|
||||||
border: 1px solid ${props => props.theme.border};
|
|
||||||
// border-collapse: collapse;
|
|
||||||
padding: 2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
${props =>
|
|
||||||
props.isSelected &&
|
|
||||||
!props.isAutofillSelected &&
|
|
||||||
!props.isFocusedColumn &&
|
|
||||||
`
|
|
||||||
background: initial;
|
|
||||||
background-color: ${props.theme.gridbody_selection[4]};
|
|
||||||
color: ${props.theme.gridbody_invfont1};`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
props.isFrameSelected &&
|
|
||||||
`
|
|
||||||
outline: 3px solid ${props.theme.gridbody_selection[4]};
|
|
||||||
outline-offset: -3px;`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
props.isAutofillSelected &&
|
|
||||||
!props.isFocusedColumn &&
|
|
||||||
`
|
|
||||||
outline: 3px solid ${props.theme.gridbody_selection[4]};
|
|
||||||
outline-offset: -3px;`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
props.isModifiedRow &&
|
|
||||||
!props.isInsertedRow &&
|
|
||||||
!props.isSelected &&
|
|
||||||
!props.isAutofillSelected &&
|
|
||||||
!props.isModifiedCell &&
|
|
||||||
!props.isFocusedColumn &&
|
|
||||||
`
|
|
||||||
background-color: ${props.theme.gridbody_background_gold[1]};`}
|
|
||||||
${props =>
|
|
||||||
!props.isSelected &&
|
|
||||||
!props.isAutofillSelected &&
|
|
||||||
!props.isInsertedRow &&
|
|
||||||
!props.isFocusedColumn &&
|
|
||||||
props.isModifiedCell &&
|
|
||||||
`
|
|
||||||
background-color: ${props.theme.gridbody_background_orange[1]};`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
!props.isSelected &&
|
|
||||||
!props.isAutofillSelected &&
|
|
||||||
!props.isFocusedColumn &&
|
|
||||||
props.isInsertedRow &&
|
|
||||||
`
|
|
||||||
background-color: ${props.theme.gridbody_background_green[1]};`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
!props.isSelected &&
|
|
||||||
!props.isAutofillSelected &&
|
|
||||||
!props.isFocusedColumn &&
|
|
||||||
props.isDeletedRow &&
|
|
||||||
`
|
|
||||||
background-color: ${props.theme.gridbody_background_volcano[1]};
|
|
||||||
`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
props.isFocusedColumn &&
|
|
||||||
`
|
|
||||||
background-color: ${props.theme.gridbody_background_yellow[0]};
|
|
||||||
`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
props.isDeletedRow &&
|
|
||||||
`
|
|
||||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
|
||||||
// from http://www.patternify.com/
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
background-position: 50% 50%;`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HintSpan = styled.span`
|
|
||||||
color: ${props => props.theme.gridbody_font3};
|
|
||||||
margin-left: 5px;
|
|
||||||
`;
|
|
||||||
const NullSpan = styled.span`
|
|
||||||
color: ${props => props.theme.gridbody_font3};
|
|
||||||
font-style: italic;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TableBodyRow = styled.tr`
|
|
||||||
// height: 35px;
|
|
||||||
background-color: ${props => props.theme.gridbody_background};
|
|
||||||
&:nth-child(6n + 3) {
|
|
||||||
background-color: ${props => props.theme.gridbody_background_alt2};
|
|
||||||
}
|
|
||||||
&:nth-child(6n + 6) {
|
|
||||||
background-color: ${props => props.theme.gridbody_background_alt3};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TableHeaderCell = styled.td`
|
|
||||||
border: 1px solid ${props => props.theme.border};
|
|
||||||
text-align: left;
|
|
||||||
padding: 2px;
|
|
||||||
background-color: ${props => props.theme.gridheader_background};
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AutoFillPoint = styled.div`
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background-color: ${props => props.theme.gridbody_selection[6]};
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
overflow: visible;
|
|
||||||
cursor: crosshair;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ShowFormButton = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
top: 1px;
|
|
||||||
color: ${props => props.theme.gridbody_font3};
|
|
||||||
background-color: ${props => props.theme.gridheader_background};
|
|
||||||
border: 1px solid ${props => props.theme.gridheader_background};
|
|
||||||
&:hover {
|
|
||||||
color: ${props => props.theme.gridheader_font_hover};
|
|
||||||
border: 1px solid ${props => props.theme.border};
|
|
||||||
top: 1px;
|
|
||||||
right: 0px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function makeBulletString(value) {
|
|
||||||
return _.pad('', value.length, '•');
|
|
||||||
}
|
|
||||||
|
|
||||||
function highlightSpecialCharacters(value) {
|
|
||||||
value = value.replace(/\n/g, '↲');
|
|
||||||
value = value.replace(/\r/g, '');
|
|
||||||
value = value.replace(/^(\s+)/, makeBulletString);
|
|
||||||
value = value.replace(/(\s+)$/, makeBulletString);
|
|
||||||
value = value.replace(/(\s\s+)/g, makeBulletString);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
|
||||||
|
|
||||||
export function CellFormattedValue({ value, dataType, theme }) {
|
|
||||||
if (value == null) return <NullSpan theme={theme}>(NULL)</NullSpan>;
|
|
||||||
if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
if (value === true) return '1';
|
|
||||||
if (value === false) return '0';
|
|
||||||
if (_.isNumber(value)) {
|
|
||||||
if (value >= 10000 || value <= -10000) {
|
|
||||||
return value.toLocaleString();
|
|
||||||
}
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
if (_.isString(value)) {
|
|
||||||
if (dateTimeRegex.test(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
return highlightSpecialCharacters(value);
|
|
||||||
}
|
|
||||||
if (_.isPlainObject(value)) {
|
|
||||||
if (_.isArray(value.data)) {
|
|
||||||
if (value.data.length == 1 && isTypeLogical(dataType)) return value.data[0];
|
|
||||||
return <NullSpan theme={theme}>({value.data.length} bytes)</NullSpan>;
|
|
||||||
}
|
|
||||||
return <NullSpan theme={theme}>(RAW)</NullSpan>;
|
|
||||||
}
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function RowHeaderCell({ rowIndex, theme, onSetFormView, rowData }) {
|
|
||||||
const [mouseIn, setMouseIn] = React.useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableHeaderCell
|
|
||||||
data-row={rowIndex}
|
|
||||||
data-col="header"
|
|
||||||
theme={theme}
|
|
||||||
onMouseEnter={onSetFormView ? () => setMouseIn(true) : null}
|
|
||||||
onMouseLeave={onSetFormView ? () => setMouseIn(false) : null}
|
|
||||||
>
|
|
||||||
{rowIndex + 1}
|
|
||||||
{!!onSetFormView && mouseIn && (
|
|
||||||
<ShowFormButton
|
|
||||||
theme={theme}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSetFormView(rowData);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontIcon icon="icon form" />
|
|
||||||
</ShowFormButton>
|
|
||||||
)}
|
|
||||||
</TableHeaderCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param props {import('./types').DataGridProps} */
|
|
||||||
function DataGridRow(props) {
|
|
||||||
const {
|
|
||||||
rowHeight,
|
|
||||||
rowIndex,
|
|
||||||
visibleRealColumns,
|
|
||||||
inplaceEditorState,
|
|
||||||
dispatchInsplaceEditor,
|
|
||||||
autofillMarkerCell,
|
|
||||||
selectedCells,
|
|
||||||
autofillSelectedCells,
|
|
||||||
focusedColumn,
|
|
||||||
grider,
|
|
||||||
frameSelection,
|
|
||||||
onSetFormView,
|
|
||||||
} = props;
|
|
||||||
// usePropsCompare({
|
|
||||||
// rowHeight,
|
|
||||||
// rowIndex,
|
|
||||||
// visibleRealColumns,
|
|
||||||
// inplaceEditorState,
|
|
||||||
// dispatchInsplaceEditor,
|
|
||||||
// row,
|
|
||||||
// display,
|
|
||||||
// changeSet,
|
|
||||||
// setChangeSet,
|
|
||||||
// insertedRowIndex,
|
|
||||||
// autofillMarkerCell,
|
|
||||||
// selectedCells,
|
|
||||||
// autofillSelectedCells,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// console.log('RENDER ROW', rowIndex);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const rowData = grider.getRowData(rowIndex);
|
|
||||||
const rowStatus = grider.getRowStatus(rowIndex);
|
|
||||||
|
|
||||||
const hintFieldsAllowed = visibleRealColumns
|
|
||||||
.filter(col => {
|
|
||||||
if (!col.hintColumnName) return false;
|
|
||||||
if (rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map(col => col.uniqueName);
|
|
||||||
|
|
||||||
if (!rowData) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableBodyRow style={{ height: `${rowHeight}px` }} theme={theme}>
|
|
||||||
<RowHeaderCell rowIndex={rowIndex} theme={theme} onSetFormView={onSetFormView} rowData={rowData} />
|
|
||||||
|
|
||||||
{visibleRealColumns.map(col => (
|
|
||||||
<TableBodyCell
|
|
||||||
key={col.uniqueName}
|
|
||||||
theme={theme}
|
|
||||||
style={{
|
|
||||||
width: col.widthPx,
|
|
||||||
minWidth: col.widthPx,
|
|
||||||
maxWidth: col.widthPx,
|
|
||||||
}}
|
|
||||||
data-row={rowIndex}
|
|
||||||
data-col={col.colIndex}
|
|
||||||
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
|
|
||||||
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
|
|
||||||
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
|
|
||||||
isModifiedRow={rowStatus.status == 'updated'}
|
|
||||||
isFocusedColumn={col.uniqueName == focusedColumn}
|
|
||||||
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
|
||||||
isInsertedRow={
|
|
||||||
rowStatus.status == 'inserted' || (rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))
|
|
||||||
}
|
|
||||||
isDeletedRow={
|
|
||||||
rowStatus.status == 'deleted' || (rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{inplaceEditorState.cell &&
|
|
||||||
rowIndex == inplaceEditorState.cell[0] &&
|
|
||||||
col.colIndex == inplaceEditorState.cell[1] ? (
|
|
||||||
<InplaceEditor
|
|
||||||
widthPx={col.widthPx}
|
|
||||||
inplaceEditorState={inplaceEditorState}
|
|
||||||
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
|
||||||
cellValue={rowData[col.uniqueName]}
|
|
||||||
// grider={grider}
|
|
||||||
// rowIndex={rowIndex}
|
|
||||||
// uniqueName={col.uniqueName}
|
|
||||||
onSetValue={value => grider.setCellValue(rowIndex, col.uniqueName, value)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CellFormattedValue value={rowData[col.uniqueName]} dataType={col.dataType} theme={theme} />
|
|
||||||
{hintFieldsAllowed.includes(col.uniqueName) && (
|
|
||||||
<HintSpan theme={theme}>{rowData[col.hintColumnName]}</HintSpan>
|
|
||||||
)}
|
|
||||||
{col.foreignKey && rowData[col.uniqueName] && (
|
|
||||||
<ShowFormButton
|
|
||||||
theme={theme}
|
|
||||||
className="buttonLike"
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSetFormView(rowData, col);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontIcon icon="icon form" />
|
|
||||||
</ShowFormButton>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{autofillMarkerCell && autofillMarkerCell[1] == col.colIndex && autofillMarkerCell[0] == rowIndex && (
|
|
||||||
<AutoFillPoint className="autofillHandleMarker" theme={theme}></AutoFillPoint>
|
|
||||||
)}
|
|
||||||
</TableBodyCell>
|
|
||||||
))}
|
|
||||||
</TableBodyRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(DataGridRow);
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ToolbarButton from '../widgets/ToolbarButton';
|
|
||||||
|
|
||||||
export default function DataGridToolbar({ reload, reconnect, grider, save, switchToForm }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{switchToForm && (
|
|
||||||
<ToolbarButton onClick={switchToForm} icon="icon form">
|
|
||||||
Form view
|
|
||||||
</ToolbarButton>
|
|
||||||
)}
|
|
||||||
<ToolbarButton onClick={reload} icon="icon reload">
|
|
||||||
Refresh
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton onClick={reconnect} icon="icon connection">
|
|
||||||
Reconnect
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!grider.canUndo} onClick={() => grider.undo()} icon="icon undo">
|
|
||||||
Undo
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!grider.canRedo} onClick={() => grider.redo()} icon="icon redo">
|
|
||||||
Redo
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!grider.allowSave} onClick={save} icon="icon save">
|
|
||||||
Save
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!grider.containsChanges} onClick={() => grider.revertAllChanges()} icon="icon close">
|
|
||||||
Revert
|
|
||||||
</ToolbarButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
export interface GriderRowStatus {
|
|
||||||
status: 'regular' | 'updated' | 'deleted' | 'inserted';
|
|
||||||
modifiedFields?: Set<string>;
|
|
||||||
insertedFields?: Set<string>;
|
|
||||||
deletedFields?: Set<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default abstract class Grider {
|
|
||||||
abstract getRowData(index): any;
|
|
||||||
abstract get rowCount(): number;
|
|
||||||
|
|
||||||
getRowStatus(index): GriderRowStatus {
|
|
||||||
const res: GriderRowStatus = {
|
|
||||||
status: 'regular',
|
|
||||||
};
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
beginUpdate() {}
|
|
||||||
endUpdate() {}
|
|
||||||
setCellValue(index: number, uniqueName: string, value: any) {}
|
|
||||||
deleteRow(index: number) {}
|
|
||||||
insertRow(): number {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
revertRowChanges(index: number) {}
|
|
||||||
revertAllChanges() {}
|
|
||||||
undo() {}
|
|
||||||
redo() {}
|
|
||||||
get editable() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get canInsert() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get allowSave() {
|
|
||||||
return this.containsChanges;
|
|
||||||
}
|
|
||||||
get rowCountInUpdate() {
|
|
||||||
return this.rowCount;
|
|
||||||
}
|
|
||||||
get canUndo() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get canRedo() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get containsChanges() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get disableLoadNextPage() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get errors() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
updateRow(index, changeObject) {
|
|
||||||
for (const key of Object.keys(changeObject)) {
|
|
||||||
this.setCellValue(index, key, changeObject[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import keycodes from '../utility/keycodes';
|
|
||||||
|
|
||||||
const StyledInput = styled.input`
|
|
||||||
border: 0px solid;
|
|
||||||
outline: none;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function InplaceEditor({
|
|
||||||
widthPx,
|
|
||||||
// rowIndex,
|
|
||||||
// uniqueName,
|
|
||||||
// grider,
|
|
||||||
cellValue,
|
|
||||||
inplaceEditorState,
|
|
||||||
dispatchInsplaceEditor,
|
|
||||||
onSetValue,
|
|
||||||
}) {
|
|
||||||
const editorRef = React.useRef();
|
|
||||||
const widthRef = React.useRef(widthPx);
|
|
||||||
const isChangedRef = React.useRef(!!inplaceEditorState.text);
|
|
||||||
React.useEffect(() => {
|
|
||||||
const editor = editorRef.current;
|
|
||||||
editor.value = inplaceEditorState.text || cellValue;
|
|
||||||
editor.focus();
|
|
||||||
if (inplaceEditorState.selectAll) {
|
|
||||||
editor.select();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
function handleBlur() {
|
|
||||||
if (isChangedRef.current) {
|
|
||||||
const editor = editorRef.current;
|
|
||||||
onSetValue(editor.value);
|
|
||||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
|
||||||
isChangedRef.current = false;
|
|
||||||
}
|
|
||||||
dispatchInsplaceEditor({ type: 'close' });
|
|
||||||
}
|
|
||||||
if (inplaceEditorState.shouldSave) {
|
|
||||||
const editor = editorRef.current;
|
|
||||||
if (isChangedRef.current) {
|
|
||||||
onSetValue(editor.value);
|
|
||||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
|
||||||
isChangedRef.current = false;
|
|
||||||
}
|
|
||||||
editor.blur();
|
|
||||||
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
|
|
||||||
}
|
|
||||||
function handleKeyDown(event) {
|
|
||||||
const editor = editorRef.current;
|
|
||||||
switch (event.keyCode) {
|
|
||||||
case keycodes.escape:
|
|
||||||
isChangedRef.current = false;
|
|
||||||
dispatchInsplaceEditor({ type: 'close' });
|
|
||||||
break;
|
|
||||||
case keycodes.enter:
|
|
||||||
if (isChangedRef.current) {
|
|
||||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
|
||||||
onSetValue(editor.value);
|
|
||||||
isChangedRef.current = false;
|
|
||||||
}
|
|
||||||
editor.blur();
|
|
||||||
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
|
|
||||||
break;
|
|
||||||
case keycodes.s:
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
if (isChangedRef.current) {
|
|
||||||
onSetValue(editor.value);
|
|
||||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
|
||||||
isChangedRef.current = false;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<StyledInput
|
|
||||||
onBlur={handleBlur}
|
|
||||||
ref={editorRef}
|
|
||||||
type="text"
|
|
||||||
onChange={() => (isChangedRef.current = true)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
style={{
|
|
||||||
width: widthRef.current,
|
|
||||||
minWidth: widthRef.current,
|
|
||||||
maxWidth: widthRef.current,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import { useSetOpenedTabs } from '../utility/globalState';
|
|
||||||
import useSocket from '../utility/SocketProvider';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import ImportExportModal from '../modals/ImportExportModal';
|
|
||||||
import LoadingDataGridCore from './LoadingDataGridCore';
|
|
||||||
import RowsArrayGrider from './RowsArrayGrider';
|
|
||||||
|
|
||||||
async function loadDataPage(props, offset, limit) {
|
|
||||||
const { jslid, display } = props;
|
|
||||||
|
|
||||||
const response = await axios.post('jsldata/get-rows', {
|
|
||||||
jslid,
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
filters: display ? display.compileFilters() : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dataPageAvailable(props) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRowCount(props) {
|
|
||||||
const { jslid } = props;
|
|
||||||
|
|
||||||
const response = await axios.request({
|
|
||||||
url: 'jsldata/get-stats',
|
|
||||||
method: 'get',
|
|
||||||
params: {
|
|
||||||
jslid,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return response.data.rowCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JslDataGridCore(props) {
|
|
||||||
const { jslid } = props;
|
|
||||||
const [changeIndex, setChangeIndex] = React.useState(0);
|
|
||||||
const [rowCountLoaded, setRowCountLoaded] = React.useState(null);
|
|
||||||
|
|
||||||
const showModal = useShowModal();
|
|
||||||
|
|
||||||
const setOpenedTabs = useSetOpenedTabs();
|
|
||||||
const socket = useSocket();
|
|
||||||
|
|
||||||
function exportGrid() {
|
|
||||||
const initialValues = {};
|
|
||||||
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
|
|
||||||
if (archiveMatch) {
|
|
||||||
initialValues.sourceStorageType = 'archive';
|
|
||||||
initialValues.sourceArchiveFolder = archiveMatch[1];
|
|
||||||
initialValues.sourceList = [archiveMatch[2]];
|
|
||||||
} else {
|
|
||||||
initialValues.sourceStorageType = 'jsldata';
|
|
||||||
initialValues.sourceJslId = jslid;
|
|
||||||
initialValues.sourceList = ['query-data'];
|
|
||||||
}
|
|
||||||
showModal(modalState => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleJslDataStats = React.useCallback(
|
|
||||||
stats => {
|
|
||||||
if (stats.changeIndex < changeIndex) return;
|
|
||||||
setChangeIndex(stats.changeIndex);
|
|
||||||
setRowCountLoaded(stats.rowCount);
|
|
||||||
},
|
|
||||||
[changeIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (jslid && socket) {
|
|
||||||
socket.on(`jsldata-stats-${jslid}`, handleJslDataStats);
|
|
||||||
return () => {
|
|
||||||
socket.off(`jsldata-stats-${jslid}`, handleJslDataStats);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [jslid]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LoadingDataGridCore
|
|
||||||
{...props}
|
|
||||||
exportGrid={exportGrid}
|
|
||||||
loadDataPage={loadDataPage}
|
|
||||||
dataPageAvailable={dataPageAvailable}
|
|
||||||
loadRowCount={loadRowCount}
|
|
||||||
rowCountLoaded={rowCountLoaded}
|
|
||||||
loadNextDataToken={changeIndex}
|
|
||||||
onReload={() => setChangeIndex(0)}
|
|
||||||
griderFactory={RowsArrayGrider.factory}
|
|
||||||
griderFactoryDeps={RowsArrayGrider.factoryDeps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import DataGridCore from './DataGridCore';
|
|
||||||
|
|
||||||
export default function LoadingDataGridCore(props) {
|
|
||||||
const {
|
|
||||||
display,
|
|
||||||
loadDataPage,
|
|
||||||
dataPageAvailable,
|
|
||||||
loadRowCount,
|
|
||||||
loadNextDataToken,
|
|
||||||
onReload,
|
|
||||||
exportGrid,
|
|
||||||
openQuery,
|
|
||||||
griderFactory,
|
|
||||||
griderFactoryDeps,
|
|
||||||
onChangeGrider,
|
|
||||||
rowCountLoaded,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [loadProps, setLoadProps] = React.useState({
|
|
||||||
isLoading: false,
|
|
||||||
loadedRows: [],
|
|
||||||
isLoadedAll: false,
|
|
||||||
loadedTime: new Date().getTime(),
|
|
||||||
allRowCount: null,
|
|
||||||
errorMessage: null,
|
|
||||||
loadNextDataToken: 0,
|
|
||||||
});
|
|
||||||
const { isLoading, loadedRows, isLoadedAll, loadedTime, allRowCount, errorMessage } = loadProps;
|
|
||||||
|
|
||||||
const loadedTimeRef = React.useRef(0);
|
|
||||||
|
|
||||||
const handleLoadRowCount = async () => {
|
|
||||||
const rowCount = await loadRowCount(props);
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
allRowCount: rowCount,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const reload = () => {
|
|
||||||
setLoadProps({
|
|
||||||
allRowCount: null,
|
|
||||||
isLoading: false,
|
|
||||||
loadedRows: [],
|
|
||||||
isLoadedAll: false,
|
|
||||||
loadedTime: new Date().getTime(),
|
|
||||||
errorMessage: null,
|
|
||||||
loadNextDataToken: 0,
|
|
||||||
});
|
|
||||||
if (onReload) onReload();
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (props.masterLoadedTime && props.masterLoadedTime > loadedTime) {
|
|
||||||
display.reload();
|
|
||||||
}
|
|
||||||
if (display.cache.refreshTime > loadedTime) {
|
|
||||||
reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadNextData = async () => {
|
|
||||||
if (isLoading) return;
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
isLoading: true,
|
|
||||||
}));
|
|
||||||
const loadStart = new Date().getTime();
|
|
||||||
loadedTimeRef.current = loadStart;
|
|
||||||
|
|
||||||
const nextRows = await loadDataPage(props, loadedRows.length, 100);
|
|
||||||
if (loadedTimeRef.current !== loadStart) {
|
|
||||||
// new load was dispatched
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// if (!_.isArray(nextRows)) {
|
|
||||||
// console.log('Error loading data from server', nextRows);
|
|
||||||
// nextRows = [];
|
|
||||||
// }
|
|
||||||
// console.log('nextRows', nextRows);
|
|
||||||
if (nextRows.errorMessage) {
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage: nextRows.errorMessage,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
if (allRowCount == null) handleLoadRowCount();
|
|
||||||
const loadedInfo = {
|
|
||||||
loadedRows: [...loadedRows, ...nextRows],
|
|
||||||
loadedTime,
|
|
||||||
};
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
isLoading: false,
|
|
||||||
isLoadedAll: oldLoadProps.loadNextDataToken == loadNextDataToken && nextRows.length === 0,
|
|
||||||
loadNextDataToken,
|
|
||||||
...loadedInfo,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setLoadProps(oldProps => ({
|
|
||||||
...oldProps,
|
|
||||||
isLoadedAll: false,
|
|
||||||
}));
|
|
||||||
}, [loadNextDataToken]);
|
|
||||||
|
|
||||||
const griderProps = { ...props, sourceRows: loadedRows };
|
|
||||||
const grider = React.useMemo(() => griderFactory(griderProps), griderFactoryDeps(griderProps));
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (onChangeGrider) onChangeGrider(grider);
|
|
||||||
}, [grider]);
|
|
||||||
|
|
||||||
const handleLoadNextData = () => {
|
|
||||||
if (!isLoadedAll && !errorMessage && !grider.disableLoadNextPage) {
|
|
||||||
if (dataPageAvailable(props)) {
|
|
||||||
// If not, callbacks to load missing metadata are dispatched
|
|
||||||
loadNextData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataGridCore
|
|
||||||
{...props}
|
|
||||||
loadNextData={handleLoadNextData}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
isLoadedAll={isLoadedAll}
|
|
||||||
loadedTime={loadedTime}
|
|
||||||
exportGrid={exportGrid}
|
|
||||||
allRowCount={rowCountLoaded || allRowCount}
|
|
||||||
openQuery={openQuery}
|
|
||||||
isLoading={isLoading}
|
|
||||||
grider={grider}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const ManagerInnerContainer = styled.div`
|
|
||||||
flex: 1 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: auto;
|
|
||||||
`;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ToolbarButton from '../widgets/ToolbarButton';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import dimensions from '../theme/dimensions';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background: ${props => props.theme.gridheader_background_cyan[0]};
|
|
||||||
height: ${dimensions.toolBar.height}px;
|
|
||||||
min-height: ${dimensions.toolBar.height}px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-top: 1px solid ${props => props.theme.border};
|
|
||||||
border-bottom: 1px solid ${props => props.theme.border};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Header = styled.div`
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 10px;
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HeaderText = styled.div`
|
|
||||||
margin-left: 10px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function ReferenceHeader({ reference, onClose }) {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<Container theme={theme}>
|
|
||||||
<Header>
|
|
||||||
<FontIcon icon="img reference" />
|
|
||||||
<HeaderText>
|
|
||||||
{reference.pureName} [{reference.columns.map(x => x.refName).join(', ')}] = master [
|
|
||||||
{reference.columns.map(x => x.baseName).join(', ')}]
|
|
||||||
</HeaderText>
|
|
||||||
</Header>
|
|
||||||
<ToolbarButton icon="icon close" onClick={onClose} patchY={6}>
|
|
||||||
Close
|
|
||||||
</ToolbarButton>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { ManagerInnerContainer } from './ManagerStyles';
|
|
||||||
import SearchInput from '../widgets/SearchInput';
|
|
||||||
import { filterName } from 'dbgate-datalib';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const SearchBoxWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Header = styled.div`
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LinkContainer = styled.div`
|
|
||||||
color: ${props => props.theme.manager_font_blue[7]};
|
|
||||||
margin: 5px;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NameContainer = styled.div`
|
|
||||||
margin-left: 5px;
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function ManagerRow({ tableName, columns, icon, onClick }) {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<LinkContainer onClick={onClick} theme={theme}>
|
|
||||||
<FontIcon icon={icon} />
|
|
||||||
<NameContainer>
|
|
||||||
{tableName} ({columns.map(x => x.columnName).join(', ')})
|
|
||||||
</NameContainer>
|
|
||||||
</LinkContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param props {import('./types').DataGridProps} */
|
|
||||||
export default function ReferenceManager(props) {
|
|
||||||
const [filter, setFilter] = React.useState('');
|
|
||||||
const { display } = props;
|
|
||||||
const { baseTable } = display || {};
|
|
||||||
const { foreignKeys } = baseTable || {};
|
|
||||||
const { dependencies } = baseTable || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SearchBoxWrapper>
|
|
||||||
<SearchInput placeholder="Search references" filter={filter} setFilter={setFilter} />
|
|
||||||
</SearchBoxWrapper>
|
|
||||||
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
|
|
||||||
{foreignKeys && foreignKeys.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Header>References tables ({foreignKeys.length})</Header>
|
|
||||||
{foreignKeys
|
|
||||||
.filter(fk => filterName(filter, fk.refTableName))
|
|
||||||
.map(fk => (
|
|
||||||
<ManagerRow
|
|
||||||
key={fk.constraintName}
|
|
||||||
icon="img link"
|
|
||||||
tableName={fk.refTableName}
|
|
||||||
columns={fk.columns}
|
|
||||||
onClick={() =>
|
|
||||||
props.onReferenceClick({
|
|
||||||
schemaName: fk.refSchemaName,
|
|
||||||
pureName: fk.refTableName,
|
|
||||||
columns: fk.columns.map(col => ({
|
|
||||||
baseName: col.columnName,
|
|
||||||
refName: col.refColumnName,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{dependencies && dependencies.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Header>Dependend tables ({dependencies.length})</Header>
|
|
||||||
{dependencies
|
|
||||||
.filter(fk => filterName(filter, fk.pureName))
|
|
||||||
.map(fk => (
|
|
||||||
<ManagerRow
|
|
||||||
key={fk.constraintName}
|
|
||||||
icon="img reference"
|
|
||||||
tableName={fk.pureName}
|
|
||||||
columns={fk.columns}
|
|
||||||
onClick={() =>
|
|
||||||
props.onReferenceClick({
|
|
||||||
schemaName: fk.schemaName,
|
|
||||||
pureName: fk.pureName,
|
|
||||||
columns: fk.columns.map(col => ({
|
|
||||||
baseName: col.refColumnName,
|
|
||||||
refName: col.columnName,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ManagerInnerContainer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import Grider, { GriderRowStatus } from './Grider';
|
|
||||||
|
|
||||||
export default class RowsArrayGrider extends Grider {
|
|
||||||
constructor(private rows: any[]) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
getRowData(index: any) {
|
|
||||||
return this.rows[index];
|
|
||||||
}
|
|
||||||
get rowCount() {
|
|
||||||
return this.rows.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
static factory({ sourceRows }): RowsArrayGrider {
|
|
||||||
return new RowsArrayGrider(sourceRows);
|
|
||||||
}
|
|
||||||
static factoryDeps({ sourceRows }) {
|
|
||||||
return [sourceRows];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import useDimensions from '../utility/useDimensions';
|
|
||||||
|
|
||||||
const StyledHorizontalScrollBar = styled.div`
|
|
||||||
overflow-x: scroll;
|
|
||||||
height: 16px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
//left: 100px;
|
|
||||||
// right: 20px;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledHorizontalScrollContent = styled.div``;
|
|
||||||
|
|
||||||
const StyledVerticalScrollBar = styled.div`
|
|
||||||
overflow-y: scroll;
|
|
||||||
width: 20px;
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
width: 20px;
|
|
||||||
bottom: 16px;
|
|
||||||
// bottom: 0;
|
|
||||||
top: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledVerticalScrollContent = styled.div``;
|
|
||||||
|
|
||||||
export function HorizontalScrollBar({
|
|
||||||
onScroll = undefined,
|
|
||||||
valueToSet = undefined,
|
|
||||||
valueToSetDate = undefined,
|
|
||||||
minimum,
|
|
||||||
maximum,
|
|
||||||
viewportRatio = 0.5,
|
|
||||||
}) {
|
|
||||||
const [ref, { width }, node] = useDimensions();
|
|
||||||
const contentSize = Math.round(width / viewportRatio);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
|
|
||||||
const position = position01 * (contentSize - width);
|
|
||||||
if (node) node.scrollLeft = Math.floor(position);
|
|
||||||
}, [valueToSetDate]);
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
const position = node.scrollLeft;
|
|
||||||
const ratio = position / (contentSize - width);
|
|
||||||
if (ratio < 0) return 0;
|
|
||||||
let res = ratio * (maximum - minimum + 1) + minimum;
|
|
||||||
onScroll(Math.floor(res + 0.3));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledHorizontalScrollBar ref={ref} onScroll={handleScroll}>
|
|
||||||
<StyledHorizontalScrollContent style={{ width: `${contentSize}px` }}> </StyledHorizontalScrollContent>
|
|
||||||
</StyledHorizontalScrollBar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VerticalScrollBar({
|
|
||||||
onScroll,
|
|
||||||
valueToSet = undefined,
|
|
||||||
valueToSetDate = undefined,
|
|
||||||
minimum,
|
|
||||||
maximum,
|
|
||||||
viewportRatio = 0.5,
|
|
||||||
}) {
|
|
||||||
const [ref, { height }, node] = useDimensions();
|
|
||||||
const contentSize = Math.round(height / viewportRatio);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
|
|
||||||
const position = position01 * (contentSize - height);
|
|
||||||
if (node) node.scrollTop = Math.floor(position);
|
|
||||||
}, [valueToSetDate]);
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
const position = node.scrollTop;
|
|
||||||
const ratio = position / (contentSize - height);
|
|
||||||
if (ratio < 0) return 0;
|
|
||||||
let res = ratio * (maximum - minimum + 1) + minimum;
|
|
||||||
onScroll(Math.floor(res + 0.3));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledVerticalScrollBar ref={ref} onScroll={handleScroll}>
|
|
||||||
<StyledVerticalScrollContent style={{ height: `${contentSize}px` }}> </StyledVerticalScrollContent>
|
|
||||||
</StyledVerticalScrollBar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// export interface IScrollBarProps {
|
|
||||||
// viewportRatio: number;
|
|
||||||
// minimum: number;
|
|
||||||
// maximum: number;
|
|
||||||
// containerStyle: any;
|
|
||||||
// onScroll?: any;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export abstract class ScrollBarBase extends React.Component<IScrollBarProps, {}> {
|
|
||||||
// domScrollContainer: Element;
|
|
||||||
// domScrollContent: Element;
|
|
||||||
// contentSize: number;
|
|
||||||
// containerResizedBind: any;
|
|
||||||
|
|
||||||
// constructor(props) {
|
|
||||||
// super(props);
|
|
||||||
// this.containerResizedBind = this.containerResized.bind(this);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// componentDidMount() {
|
|
||||||
// $(this.domScrollContainer).scroll(this.onScroll.bind(this));
|
|
||||||
// createResizeDetector(this.domScrollContainer, this.containerResized.bind(this));
|
|
||||||
// window.addEventListener('resize', this.containerResizedBind);
|
|
||||||
// this.updateContentSize();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// componentWillUnmount() {
|
|
||||||
// deleteResizeDetector(this.domScrollContainer);
|
|
||||||
// window.removeEventListener('resize', this.containerResizedBind);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// onScroll() {
|
|
||||||
// if (this.props.onScroll) {
|
|
||||||
// this.props.onScroll(this.value);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// get value(): number {
|
|
||||||
// let position = this.getScrollPosition();
|
|
||||||
// let ratio = position / (this.contentSize - this.getContainerSize());
|
|
||||||
// if (ratio < 0) return 0;
|
|
||||||
// let res = ratio * (this.props.maximum - this.props.minimum + 1) + this.props.minimum;
|
|
||||||
// return Math.floor(res + 0.3);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// set value(value: number) {
|
|
||||||
// let position01 = (value - this.props.minimum) / (this.props.maximum - this.props.minimum + 1);
|
|
||||||
// let position = position01 * (this.contentSize - this.getContainerSize());
|
|
||||||
// this.setScrollPosition(Math.floor(position));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// containerResized() {
|
|
||||||
// this.setContentSizeField();
|
|
||||||
// this.updateContentSize();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setContentSizeField() {
|
|
||||||
// let lastContentSize = this.contentSize;
|
|
||||||
// this.contentSize = Math.round(this.getContainerSize() / this.props.viewportRatio);
|
|
||||||
// if (_.isNaN(this.contentSize)) this.contentSize = 0;
|
|
||||||
// if (this.contentSize > 1000000 && detectBrowser() == BrowserType.Firefox) this.contentSize = 1000000;
|
|
||||||
// if (lastContentSize != this.contentSize) {
|
|
||||||
// this.updateContentSize();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// abstract getContainerSize(): number;
|
|
||||||
// abstract updateContentSize();
|
|
||||||
// abstract getScrollPosition(): number;
|
|
||||||
// abstract setScrollPosition(value: number);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export class HorizontalScrollBar extends ScrollBarBase {
|
|
||||||
// render() {
|
|
||||||
// this.setContentSizeField();
|
|
||||||
|
|
||||||
// return <div className='ReactGridHorizontalScrollBar' ref={x => this.domScrollContainer = x} style={this.props.containerStyle}>
|
|
||||||
// <div className='ReactGridHorizontalScrollContent' ref={x => this.domScrollContent = x} style={{ width: this.contentSize }}>
|
|
||||||
//
|
|
||||||
// </div>
|
|
||||||
// </div>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// getContainerSize(): number {
|
|
||||||
// return $(this.domScrollContainer).width();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// updateContentSize() {
|
|
||||||
// $(this.domScrollContent).width(this.contentSize);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// getScrollPosition() {
|
|
||||||
// return $(this.domScrollContainer).scrollLeft();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setScrollPosition(value: number) {
|
|
||||||
// $(this.domScrollContainer).scrollLeft(value);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export class VerticalScrollBar extends ScrollBarBase {
|
|
||||||
// render() {
|
|
||||||
// this.setContentSizeField();
|
|
||||||
|
|
||||||
// return <div className='ReactGridVerticalScrollBar' ref={x => this.domScrollContainer = x} style={this.props.containerStyle}>
|
|
||||||
// <div className='ReactGridVerticalScrollContent' ref={x => this.domScrollContent = x} style={{ height: this.contentSize }}>
|
|
||||||
//
|
|
||||||
// </div>
|
|
||||||
// </div>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// getContainerSize(): number {
|
|
||||||
// return $(this.domScrollContainer).height();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// updateContentSize() {
|
|
||||||
// $(this.domScrollContent).height(this.contentSize);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// getScrollPosition() {
|
|
||||||
// return $(this.domScrollContainer).scrollTop();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setScrollPosition(value: number) {
|
|
||||||
// $(this.domScrollContainer).scrollTop(value);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export class SeriesSizeItem {
|
|
||||||
constructor() {
|
|
||||||
this.scrollIndex = -1;
|
|
||||||
this.frozenIndex = -1;
|
|
||||||
this.modelIndex = 0;
|
|
||||||
this.size = 0;
|
|
||||||
this.position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// modelIndex;
|
|
||||||
// size;
|
|
||||||
// position;
|
|
||||||
|
|
||||||
get endPosition() {
|
|
||||||
return this.position + this.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SeriesSizes {
|
|
||||||
constructor() {
|
|
||||||
this.scrollItems = [];
|
|
||||||
this.sizeOverridesByModelIndex = {};
|
|
||||||
this.positions = [];
|
|
||||||
this.scrollIndexes = [];
|
|
||||||
this.frozenItems = [];
|
|
||||||
this.hiddenAndFrozenModelIndexes = null;
|
|
||||||
this.frozenModelIndexes = null;
|
|
||||||
|
|
||||||
this.count = 0;
|
|
||||||
this.maxSize = 1000;
|
|
||||||
this.defaultSize = 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
// private sizeOverridesByModelIndex: { [id] } = {};
|
|
||||||
// count;
|
|
||||||
// defaultSize;
|
|
||||||
// maxSize;
|
|
||||||
// private hiddenAndFrozenModelIndexes[] = [];
|
|
||||||
// private frozenModelIndexes[] = [];
|
|
||||||
// private hiddenModelIndexes[] = [];
|
|
||||||
// private scrollItems: SeriesSizeItem[] = [];
|
|
||||||
// private positions[] = [];
|
|
||||||
// private scrollIndexes[] = [];
|
|
||||||
// private frozenItems: SeriesSizeItem[] = [];
|
|
||||||
|
|
||||||
get scrollCount() {
|
|
||||||
return this.count - (this.hiddenAndFrozenModelIndexes != null ? this.hiddenAndFrozenModelIndexes.length : 0);
|
|
||||||
}
|
|
||||||
get frozenCount() {
|
|
||||||
return this.frozenModelIndexes != null ? this.frozenModelIndexes.length : 0;
|
|
||||||
}
|
|
||||||
get frozenSize() {
|
|
||||||
return _.sumBy(this.frozenItems, x => x.size);
|
|
||||||
}
|
|
||||||
get realCount() {
|
|
||||||
return this.frozenCount + this.scrollCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
putSizeOverride(modelIndex, size, sizeByUser = false) {
|
|
||||||
if (this.maxSize && size > this.maxSize && !sizeByUser) {
|
|
||||||
size = this.maxSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentSize = this.sizeOverridesByModelIndex[modelIndex];
|
|
||||||
if (sizeByUser || !currentSize || size > currentSize) {
|
|
||||||
this.sizeOverridesByModelIndex[modelIndex] = size;
|
|
||||||
}
|
|
||||||
// if (!_.has(this.sizeOverridesByModelIndex, modelIndex))
|
|
||||||
// this.sizeOverridesByModelIndex[modelIndex] = size;
|
|
||||||
// if (size > this.sizeOverridesByModelIndex[modelIndex])
|
|
||||||
// this.sizeOverridesByModelIndex[modelIndex] = size;
|
|
||||||
}
|
|
||||||
buildIndex() {
|
|
||||||
this.scrollItems = [];
|
|
||||||
this.scrollIndexes = _.filter(
|
|
||||||
_.map(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelToReal(x) - this.frozenCount),
|
|
||||||
x => x >= 0
|
|
||||||
);
|
|
||||||
this.scrollIndexes.sort();
|
|
||||||
let lastScrollIndex = -1;
|
|
||||||
let lastEndPosition = 0;
|
|
||||||
this.scrollIndexes.forEach(scrollIndex => {
|
|
||||||
let modelIndex = this.realToModel(scrollIndex + this.frozenCount);
|
|
||||||
let size = this.sizeOverridesByModelIndex[modelIndex];
|
|
||||||
let item = new SeriesSizeItem();
|
|
||||||
item.scrollIndex = scrollIndex;
|
|
||||||
item.modelIndex = modelIndex;
|
|
||||||
item.size = size;
|
|
||||||
item.position = lastEndPosition + (scrollIndex - lastScrollIndex - 1) * this.defaultSize;
|
|
||||||
this.scrollItems.push(item);
|
|
||||||
lastScrollIndex = scrollIndex;
|
|
||||||
lastEndPosition = item.endPosition;
|
|
||||||
});
|
|
||||||
this.positions = _.map(this.scrollItems, x => x.position);
|
|
||||||
this.frozenItems = [];
|
|
||||||
let lastpos = 0;
|
|
||||||
for (let i = 0; i < this.frozenCount; i++) {
|
|
||||||
let modelIndex = this.frozenModelIndexes[i];
|
|
||||||
let size = this.getSizeByModelIndex(modelIndex);
|
|
||||||
let item = new SeriesSizeItem();
|
|
||||||
item.frozenIndex = i;
|
|
||||||
item.modelIndex = modelIndex;
|
|
||||||
item.size = size;
|
|
||||||
item.position = lastpos;
|
|
||||||
this.frozenItems.push(item);
|
|
||||||
lastpos += size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getScrollIndexOnPosition(position) {
|
|
||||||
let itemOrder = _.sortedIndex(this.positions, position);
|
|
||||||
if (this.positions[itemOrder] == position) return itemOrder;
|
|
||||||
if (itemOrder == 0) return Math.floor(position / this.defaultSize);
|
|
||||||
if (position <= this.scrollItems[itemOrder - 1].endPosition) return this.scrollItems[itemOrder - 1].scrollIndex;
|
|
||||||
return (
|
|
||||||
Math.floor((position - this.scrollItems[itemOrder - 1].position) / this.defaultSize) +
|
|
||||||
this.scrollItems[itemOrder - 1].scrollIndex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
getFrozenIndexOnPosition(position) {
|
|
||||||
this.frozenItems.forEach(function (item) {
|
|
||||||
if (position >= item.position && position <= item.endPosition) return item.frozenIndex;
|
|
||||||
});
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
// getSizeSum(startScrollIndex, endScrollIndex) {
|
|
||||||
// let order1 = _.sortedIndexOf(this.scrollIndexes, startScrollIndex);
|
|
||||||
// let order2 = _.sortedIndexOf(this.scrollIndexes, endScrollIndex);
|
|
||||||
// let count = endScrollIndex - startScrollIndex;
|
|
||||||
// if (order1 < 0)
|
|
||||||
// order1 = ~order1;
|
|
||||||
// if (order2 < 0)
|
|
||||||
// order2 = ~order2;
|
|
||||||
// let result = 0;
|
|
||||||
// for (let i = order1; i <= order2; i++) {
|
|
||||||
// if (i < 0)
|
|
||||||
// continue;
|
|
||||||
// if (i >= this.scrollItems.length)
|
|
||||||
// continue;
|
|
||||||
// let item = this.scrollItems[i];
|
|
||||||
// if (item.scrollIndex < startScrollIndex)
|
|
||||||
// continue;
|
|
||||||
// if (item.scrollIndex >= endScrollIndex)
|
|
||||||
// continue;
|
|
||||||
// result += item.size;
|
|
||||||
// count--;
|
|
||||||
// }
|
|
||||||
// result += count * this.defaultSize;
|
|
||||||
// return result;
|
|
||||||
// }
|
|
||||||
getSizeByModelIndex(modelIndex) {
|
|
||||||
if (_.has(this.sizeOverridesByModelIndex, modelIndex)) return this.sizeOverridesByModelIndex[modelIndex];
|
|
||||||
return this.defaultSize;
|
|
||||||
}
|
|
||||||
getSizeByScrollIndex(scrollIndex) {
|
|
||||||
return this.getSizeByRealIndex(scrollIndex + this.frozenCount);
|
|
||||||
}
|
|
||||||
getSizeByRealIndex(realIndex) {
|
|
||||||
let modelIndex = this.realToModel(realIndex);
|
|
||||||
return this.getSizeByModelIndex(modelIndex);
|
|
||||||
}
|
|
||||||
removeSizeOverride(realIndex) {
|
|
||||||
let modelIndex = this.realToModel(realIndex);
|
|
||||||
delete this.sizeOverridesByModelIndex[modelIndex];
|
|
||||||
}
|
|
||||||
getScroll(sourceScrollIndex, targetScrollIndex) {
|
|
||||||
if (sourceScrollIndex < targetScrollIndex) {
|
|
||||||
return -_.sum(
|
|
||||||
_.map(_.range(sourceScrollIndex, targetScrollIndex - sourceScrollIndex), x => this.getSizeByScrollIndex(x))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return _.sum(
|
|
||||||
_.map(_.range(targetScrollIndex, sourceScrollIndex - targetScrollIndex), x => this.getSizeByScrollIndex(x))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modelIndexIsInScrollArea(modelIndex) {
|
|
||||||
let realIndex = this.modelToReal(modelIndex);
|
|
||||||
return realIndex >= this.frozenCount;
|
|
||||||
}
|
|
||||||
getTotalScrollSizeSum() {
|
|
||||||
let scrollSizeOverrides = _.map(
|
|
||||||
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelIndexIsInScrollArea(x)),
|
|
||||||
x => this.sizeOverridesByModelIndex[x]
|
|
||||||
);
|
|
||||||
return _.sum(scrollSizeOverrides) + (this.count - scrollSizeOverrides.length) * this.defaultSize;
|
|
||||||
}
|
|
||||||
getVisibleScrollSizeSum() {
|
|
||||||
let scrollSizeOverrides = _.map(
|
|
||||||
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => !_.includes(this.hiddenAndFrozenModelIndexes, x)),
|
|
||||||
x => this.sizeOverridesByModelIndex[x]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
_.sum(scrollSizeOverrides) +
|
|
||||||
(this.count - this.hiddenModelIndexes.length - scrollSizeOverrides.length) * this.defaultSize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
intKeys(value) {
|
|
||||||
return _.keys(value).map(x => _.parseInt(x));
|
|
||||||
}
|
|
||||||
getPositionByRealIndex(realIndex) {
|
|
||||||
if (realIndex < 0) return 0;
|
|
||||||
if (realIndex < this.frozenCount) return this.frozenItems[realIndex].position;
|
|
||||||
return this.getPositionByScrollIndex(realIndex - this.frozenCount);
|
|
||||||
}
|
|
||||||
getPositionByScrollIndex(scrollIndex) {
|
|
||||||
let order = _.sortedIndex(this.scrollIndexes, scrollIndex);
|
|
||||||
if (this.scrollIndexes[order] == scrollIndex) return this.scrollItems[order].position;
|
|
||||||
order--;
|
|
||||||
if (order < 0) return scrollIndex * this.defaultSize;
|
|
||||||
return (
|
|
||||||
this.scrollItems[order].endPosition + (scrollIndex - this.scrollItems[order].scrollIndex - 1) * this.defaultSize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
getVisibleScrollCount(firstVisibleIndex, viewportSize) {
|
|
||||||
let res = 0;
|
|
||||||
let index = firstVisibleIndex;
|
|
||||||
let count = 0;
|
|
||||||
while (res < viewportSize && index <= this.scrollCount) {
|
|
||||||
// console.log('this.getSizeByScrollIndex(index)', this.getSizeByScrollIndex(index));
|
|
||||||
res += this.getSizeByScrollIndex(index);
|
|
||||||
index++;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
// console.log('getVisibleScrollCount', firstVisibleIndex, viewportSize, count);
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
getVisibleScrollCountReversed(lastVisibleIndex, viewportSize) {
|
|
||||||
let res = 0;
|
|
||||||
let index = lastVisibleIndex;
|
|
||||||
let count = 0;
|
|
||||||
while (res < viewportSize && index >= 0) {
|
|
||||||
res += this.getSizeByScrollIndex(index);
|
|
||||||
index--;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
invalidateAfterScroll(oldFirstVisible, newFirstVisible, invalidate, viewportSize) {
|
|
||||||
if (newFirstVisible > oldFirstVisible) {
|
|
||||||
let oldVisibleCount = this.getVisibleScrollCount(oldFirstVisible, viewportSize);
|
|
||||||
let newVisibleCount = this.getVisibleScrollCount(newFirstVisible, viewportSize);
|
|
||||||
for (let i = oldFirstVisible + oldVisibleCount - 1; i <= newFirstVisible + newVisibleCount; i++) {
|
|
||||||
invalidate(i + this.frozenCount);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = newFirstVisible; i <= oldFirstVisible; i++) {
|
|
||||||
invalidate(i + this.frozenCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isWholeInView(firstVisibleIndex, index, viewportSize) {
|
|
||||||
let res = 0;
|
|
||||||
let testedIndex = firstVisibleIndex;
|
|
||||||
while (res < viewportSize && testedIndex < this.count) {
|
|
||||||
res += this.getSizeByScrollIndex(testedIndex);
|
|
||||||
if (testedIndex == index) return res <= viewportSize;
|
|
||||||
testedIndex++;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
scrollInView(firstVisibleIndex, scrollIndex, viewportSize) {
|
|
||||||
if (this.isWholeInView(firstVisibleIndex, scrollIndex, viewportSize)) {
|
|
||||||
return firstVisibleIndex;
|
|
||||||
}
|
|
||||||
if (scrollIndex < firstVisibleIndex) {
|
|
||||||
return scrollIndex;
|
|
||||||
}
|
|
||||||
let res = 0;
|
|
||||||
let testedIndex = scrollIndex;
|
|
||||||
while (res < viewportSize && testedIndex >= 0) {
|
|
||||||
let size = this.getSizeByScrollIndex(testedIndex);
|
|
||||||
if (res + size > viewportSize) return testedIndex + 1;
|
|
||||||
testedIndex--;
|
|
||||||
res += size;
|
|
||||||
}
|
|
||||||
if (res >= viewportSize && testedIndex < scrollIndex) return testedIndex + 1;
|
|
||||||
return firstVisibleIndex;
|
|
||||||
}
|
|
||||||
resize(realIndex, newSize) {
|
|
||||||
if (realIndex < 0) return;
|
|
||||||
let modelIndex = this.realToModel(realIndex);
|
|
||||||
if (modelIndex < 0) return;
|
|
||||||
this.sizeOverridesByModelIndex[modelIndex] = newSize;
|
|
||||||
this.buildIndex();
|
|
||||||
}
|
|
||||||
setExtraordinaryIndexes(hidden, frozen) {
|
|
||||||
//this._hiddenAndFrozenModelIndexes = _.clone(hidden);
|
|
||||||
hidden = hidden.filter(x => x >= 0);
|
|
||||||
frozen = frozen.filter(x => x >= 0);
|
|
||||||
|
|
||||||
hidden.sort((a, b) => a - b);
|
|
||||||
frozen.sort((a, b) => a - b);
|
|
||||||
this.frozenModelIndexes = _.filter(frozen, x => !_.includes(hidden, x));
|
|
||||||
this.hiddenModelIndexes = _.filter(hidden, x => !_.includes(frozen, x));
|
|
||||||
this.hiddenAndFrozenModelIndexes = _.concat(hidden, this.frozenModelIndexes);
|
|
||||||
this.frozenModelIndexes.sort((a, b) => a - b);
|
|
||||||
if (this.hiddenAndFrozenModelIndexes.length == 0) this.hiddenAndFrozenModelIndexes = null;
|
|
||||||
if (this.frozenModelIndexes.length == 0) this.frozenModelIndexes = null;
|
|
||||||
this.buildIndex();
|
|
||||||
}
|
|
||||||
realToModel(realIndex) {
|
|
||||||
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return realIndex;
|
|
||||||
if (realIndex < 0) return -1;
|
|
||||||
if (realIndex < this.frozenCount && this.frozenModelIndexes != null) return this.frozenModelIndexes[realIndex];
|
|
||||||
if (this.hiddenAndFrozenModelIndexes == null) return realIndex;
|
|
||||||
realIndex -= this.frozenCount;
|
|
||||||
for (let hidItem of this.hiddenAndFrozenModelIndexes) {
|
|
||||||
if (realIndex < hidItem) return realIndex;
|
|
||||||
realIndex++;
|
|
||||||
}
|
|
||||||
return realIndex;
|
|
||||||
}
|
|
||||||
modelToReal(modelIndex) {
|
|
||||||
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return modelIndex;
|
|
||||||
if (modelIndex < 0) return -1;
|
|
||||||
let frozenIndex = this.frozenModelIndexes != null ? _.indexOf(this.frozenModelIndexes, modelIndex) : -1;
|
|
||||||
if (frozenIndex >= 0) return frozenIndex;
|
|
||||||
if (this.hiddenAndFrozenModelIndexes == null) return modelIndex;
|
|
||||||
let hiddenIndex = _.sortedIndex(this.hiddenAndFrozenModelIndexes, modelIndex);
|
|
||||||
if (this.hiddenAndFrozenModelIndexes[hiddenIndex] == modelIndex) return -1;
|
|
||||||
if (hiddenIndex >= 0) return modelIndex - hiddenIndex + this.frozenCount;
|
|
||||||
return modelIndex;
|
|
||||||
}
|
|
||||||
getFrozenPosition(frozenIndex) {
|
|
||||||
return this.frozenItems[frozenIndex].position;
|
|
||||||
}
|
|
||||||
hasSizeOverride(modelIndex) {
|
|
||||||
return _.has(this.sizeOverridesByModelIndex, modelIndex);
|
|
||||||
}
|
|
||||||
isVisible(testedRealIndex, firstVisibleScrollIndex, viewportSize) {
|
|
||||||
if (testedRealIndex < 0) return false;
|
|
||||||
if (testedRealIndex >= 0 && testedRealIndex < this.frozenCount) return true;
|
|
||||||
let scrollIndex = testedRealIndex - this.frozenCount;
|
|
||||||
let onPageIndex = scrollIndex - firstVisibleScrollIndex;
|
|
||||||
return onPageIndex >= 0 && onPageIndex < this.getVisibleScrollCount(firstVisibleScrollIndex, viewportSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export class SeriesSizeItem {
|
|
||||||
public scrollIndex: number = -1;
|
|
||||||
public modelIndex: number;
|
|
||||||
public frozenIndex: number = -1;
|
|
||||||
public size: number;
|
|
||||||
public position: number;
|
|
||||||
public get endPosition(): number {
|
|
||||||
return this.position + this.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SeriesSizes {
|
|
||||||
private sizeOverridesByModelIndex: { [id: number]: number } = {};
|
|
||||||
public count: number = 0;
|
|
||||||
public defaultSize: number = 50;
|
|
||||||
public maxSize: number = 1000;
|
|
||||||
private hiddenAndFrozenModelIndexes: number[] = [];
|
|
||||||
private frozenModelIndexes: number[] = [];
|
|
||||||
private hiddenModelIndexes: number[] = [];
|
|
||||||
private scrollItems: SeriesSizeItem[] = [];
|
|
||||||
private positions: number[] = [];
|
|
||||||
private scrollIndexes: number[] = [];
|
|
||||||
private frozenItems: SeriesSizeItem[] = [];
|
|
||||||
|
|
||||||
public get scrollCount(): number {
|
|
||||||
return this.count - (this.hiddenAndFrozenModelIndexes != null ? this.hiddenAndFrozenModelIndexes.length : 0);
|
|
||||||
}
|
|
||||||
public get frozenCount(): number {
|
|
||||||
return this.frozenModelIndexes != null ? this.frozenModelIndexes.length : 0;
|
|
||||||
}
|
|
||||||
public get frozenSize(): number {
|
|
||||||
return _.sumBy(this.frozenItems, x => x.size);
|
|
||||||
}
|
|
||||||
public get realCount(): number {
|
|
||||||
return this.frozenCount + this.scrollCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// public clear(): void {
|
|
||||||
// this.scrollItems = [];
|
|
||||||
// this.sizeOverridesByModelIndex = {};
|
|
||||||
// this.positions = [];
|
|
||||||
// this.scrollIndexes = [];
|
|
||||||
// this.frozenItems = [];
|
|
||||||
// this.hiddenAndFrozenModelIndexes = null;
|
|
||||||
// this.frozenModelIndexes = null;
|
|
||||||
// }
|
|
||||||
public putSizeOverride(modelIndex: number, size: number, sizeByUser = false): void {
|
|
||||||
if (this.maxSize && size > this.maxSize && !sizeByUser) {
|
|
||||||
size = this.maxSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentSize = this.sizeOverridesByModelIndex[modelIndex];
|
|
||||||
if (sizeByUser || !currentSize || size > currentSize) {
|
|
||||||
this.sizeOverridesByModelIndex[modelIndex] = size;
|
|
||||||
}
|
|
||||||
// if (!_.has(this.sizeOverridesByModelIndex, modelIndex))
|
|
||||||
// this.sizeOverridesByModelIndex[modelIndex] = size;
|
|
||||||
// if (size > this.sizeOverridesByModelIndex[modelIndex])
|
|
||||||
// this.sizeOverridesByModelIndex[modelIndex] = size;
|
|
||||||
}
|
|
||||||
public buildIndex(): void {
|
|
||||||
this.scrollItems = [];
|
|
||||||
this.scrollIndexes = _.filter(
|
|
||||||
_.map(_.range(this.count), x => this.modelToReal(x) - this.frozenCount),
|
|
||||||
// _.map(this.intKeys(_.keys(this.sizeOverridesByModelIndex)), (x) => this.modelToReal(x) - this.frozenCount),
|
|
||||||
x => x >= 0
|
|
||||||
);
|
|
||||||
this.scrollIndexes.sort();
|
|
||||||
let lastScrollIndex: number = -1;
|
|
||||||
let lastEndPosition: number = 0;
|
|
||||||
this.scrollIndexes.forEach(scrollIndex => {
|
|
||||||
let modelIndex: number = this.realToModel(scrollIndex + this.frozenCount);
|
|
||||||
let size: number = this.sizeOverridesByModelIndex[modelIndex];
|
|
||||||
let item = new SeriesSizeItem();
|
|
||||||
item.scrollIndex = scrollIndex;
|
|
||||||
item.modelIndex = modelIndex;
|
|
||||||
item.size = size;
|
|
||||||
item.position = lastEndPosition + (scrollIndex - lastScrollIndex - 1) * this.defaultSize;
|
|
||||||
this.scrollItems.push(item);
|
|
||||||
lastScrollIndex = scrollIndex;
|
|
||||||
lastEndPosition = item.endPosition;
|
|
||||||
});
|
|
||||||
this.positions = _.map(this.scrollItems, x => x.position);
|
|
||||||
this.frozenItems = [];
|
|
||||||
let lastpos: number = 0;
|
|
||||||
for (let i: number = 0; i < this.frozenCount; i++) {
|
|
||||||
let modelIndex: number = this.frozenModelIndexes[i];
|
|
||||||
let size: number = this.getSizeByModelIndex(modelIndex);
|
|
||||||
let item = new SeriesSizeItem();
|
|
||||||
item.frozenIndex = i;
|
|
||||||
item.modelIndex = modelIndex;
|
|
||||||
item.size = size;
|
|
||||||
item.position = lastpos;
|
|
||||||
this.frozenItems.push(item);
|
|
||||||
lastpos += size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getScrollIndexOnPosition(position: number): number {
|
|
||||||
let itemOrder: number = _.sortedIndex(this.positions, position);
|
|
||||||
if (this.positions[itemOrder] == position) return itemOrder;
|
|
||||||
if (itemOrder == 0) return Math.floor(position / this.defaultSize);
|
|
||||||
if (position <= this.scrollItems[itemOrder - 1].endPosition) return this.scrollItems[itemOrder - 1].scrollIndex;
|
|
||||||
return (
|
|
||||||
Math.floor((position - this.scrollItems[itemOrder - 1].position) / this.defaultSize) +
|
|
||||||
this.scrollItems[itemOrder - 1].scrollIndex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
public getFrozenIndexOnPosition(position: number): number {
|
|
||||||
this.frozenItems.forEach(function (item) {
|
|
||||||
if (position >= item.position && position <= item.endPosition) return item.frozenIndex;
|
|
||||||
});
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
// public getSizeSum(startScrollIndex: number, endScrollIndex: number): number {
|
|
||||||
// let order1: number = _.sortedIndexOf(this.scrollIndexes, startScrollIndex);
|
|
||||||
// let order2: number = _.sortedIndexOf(this.scrollIndexes, endScrollIndex);
|
|
||||||
// let count: number = endScrollIndex - startScrollIndex;
|
|
||||||
// if (order1 < 0)
|
|
||||||
// order1 = ~order1;
|
|
||||||
// if (order2 < 0)
|
|
||||||
// order2 = ~order2;
|
|
||||||
// let result: number = 0;
|
|
||||||
// for (let i: number = order1; i <= order2; i++) {
|
|
||||||
// if (i < 0)
|
|
||||||
// continue;
|
|
||||||
// if (i >= this.scrollItems.length)
|
|
||||||
// continue;
|
|
||||||
// let item = this.scrollItems[i];
|
|
||||||
// if (item.scrollIndex < startScrollIndex)
|
|
||||||
// continue;
|
|
||||||
// if (item.scrollIndex >= endScrollIndex)
|
|
||||||
// continue;
|
|
||||||
// result += item.size;
|
|
||||||
// count--;
|
|
||||||
// }
|
|
||||||
// result += count * this.defaultSize;
|
|
||||||
// return result;
|
|
||||||
// }
|
|
||||||
public getSizeByModelIndex(modelIndex: number): number {
|
|
||||||
if (_.has(this.sizeOverridesByModelIndex, modelIndex)) return this.sizeOverridesByModelIndex[modelIndex];
|
|
||||||
return this.defaultSize;
|
|
||||||
}
|
|
||||||
public getSizeByScrollIndex(scrollIndex: number): number {
|
|
||||||
return this.getSizeByRealIndex(scrollIndex + this.frozenCount);
|
|
||||||
}
|
|
||||||
public getSizeByRealIndex(realIndex: number): number {
|
|
||||||
let modelIndex: number = this.realToModel(realIndex);
|
|
||||||
return this.getSizeByModelIndex(modelIndex);
|
|
||||||
}
|
|
||||||
public removeSizeOverride(realIndex: number): void {
|
|
||||||
let modelIndex: number = this.realToModel(realIndex);
|
|
||||||
delete this.sizeOverridesByModelIndex[modelIndex];
|
|
||||||
}
|
|
||||||
public getScroll(sourceScrollIndex: number, targetScrollIndex: number): number {
|
|
||||||
if (sourceScrollIndex < targetScrollIndex) {
|
|
||||||
return -_.sum(
|
|
||||||
_.map(_.range(sourceScrollIndex, targetScrollIndex - sourceScrollIndex), x => this.getSizeByScrollIndex(x))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return _.sum(
|
|
||||||
_.map(_.range(targetScrollIndex, sourceScrollIndex - targetScrollIndex), x => this.getSizeByScrollIndex(x))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public modelIndexIsInScrollArea(modelIndex: number): boolean {
|
|
||||||
let realIndex = this.modelToReal(modelIndex);
|
|
||||||
return realIndex >= this.frozenCount;
|
|
||||||
}
|
|
||||||
public getTotalScrollSizeSum(): number {
|
|
||||||
let scrollSizeOverrides = _.map(
|
|
||||||
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelIndexIsInScrollArea(x)),
|
|
||||||
x => this.sizeOverridesByModelIndex[x]
|
|
||||||
);
|
|
||||||
return _.sum(scrollSizeOverrides) + (this.count - scrollSizeOverrides.length) * this.defaultSize;
|
|
||||||
}
|
|
||||||
public getVisibleScrollSizeSum(): number {
|
|
||||||
let scrollSizeOverrides = _.map(
|
|
||||||
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => !_.includes(this.hiddenAndFrozenModelIndexes, x)),
|
|
||||||
x => this.sizeOverridesByModelIndex[x]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
_.sum(scrollSizeOverrides) +
|
|
||||||
(this.count - this.hiddenModelIndexes.length - scrollSizeOverrides.length) * this.defaultSize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
private intKeys(value): number[] {
|
|
||||||
return _.keys(value).map(x => _.parseInt(x));
|
|
||||||
}
|
|
||||||
public getPositionByRealIndex(realIndex: number): number {
|
|
||||||
if (realIndex < 0) return 0;
|
|
||||||
if (realIndex < this.frozenCount) return this.frozenItems[realIndex].position;
|
|
||||||
return this.getPositionByScrollIndex(realIndex - this.frozenCount);
|
|
||||||
}
|
|
||||||
public getPositionByScrollIndex(scrollIndex: number): number {
|
|
||||||
let order: number = _.sortedIndex(this.scrollIndexes, scrollIndex);
|
|
||||||
if (this.scrollIndexes[order] == scrollIndex) return this.scrollItems[order].position;
|
|
||||||
order--;
|
|
||||||
if (order < 0) return scrollIndex * this.defaultSize;
|
|
||||||
return (
|
|
||||||
this.scrollItems[order].endPosition + (scrollIndex - this.scrollItems[order].scrollIndex - 1) * this.defaultSize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
public getVisibleScrollCount(firstVisibleIndex: number, viewportSize: number): number {
|
|
||||||
let res: number = 0;
|
|
||||||
let index: number = firstVisibleIndex;
|
|
||||||
let count: number = 0;
|
|
||||||
while (res < viewportSize && index <= this.scrollCount) {
|
|
||||||
res += this.getSizeByScrollIndex(index);
|
|
||||||
index++;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
public getVisibleScrollCountReversed(lastVisibleIndex: number, viewportSize: number): number {
|
|
||||||
let res: number = 0;
|
|
||||||
let index: number = lastVisibleIndex;
|
|
||||||
let count: number = 0;
|
|
||||||
while (res < viewportSize && index >= 0) {
|
|
||||||
res += this.getSizeByScrollIndex(index);
|
|
||||||
index--;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
public invalidateAfterScroll(
|
|
||||||
oldFirstVisible: number,
|
|
||||||
newFirstVisible: number,
|
|
||||||
invalidate: (_: number) => void,
|
|
||||||
viewportSize: number
|
|
||||||
): void {
|
|
||||||
if (newFirstVisible > oldFirstVisible) {
|
|
||||||
let oldVisibleCount: number = this.getVisibleScrollCount(oldFirstVisible, viewportSize);
|
|
||||||
let newVisibleCount: number = this.getVisibleScrollCount(newFirstVisible, viewportSize);
|
|
||||||
for (let i: number = oldFirstVisible + oldVisibleCount - 1; i <= newFirstVisible + newVisibleCount; i++) {
|
|
||||||
invalidate(i + this.frozenCount);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i: number = newFirstVisible; i <= oldFirstVisible; i++) {
|
|
||||||
invalidate(i + this.frozenCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public isWholeInView(firstVisibleIndex: number, index: number, viewportSize: number): boolean {
|
|
||||||
let res: number = 0;
|
|
||||||
let testedIndex: number = firstVisibleIndex;
|
|
||||||
while (res < viewportSize && testedIndex < this.count) {
|
|
||||||
res += this.getSizeByScrollIndex(testedIndex);
|
|
||||||
if (testedIndex == index) return res <= viewportSize;
|
|
||||||
testedIndex++;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public scrollInView(firstVisibleIndex: number, scrollIndex: number, viewportSize: number): number {
|
|
||||||
if (this.isWholeInView(firstVisibleIndex, scrollIndex, viewportSize)) {
|
|
||||||
return firstVisibleIndex;
|
|
||||||
}
|
|
||||||
if (scrollIndex < firstVisibleIndex) {
|
|
||||||
return scrollIndex;
|
|
||||||
}
|
|
||||||
let testedIndex = firstVisibleIndex + 1;
|
|
||||||
while (testedIndex < this.scrollCount) {
|
|
||||||
if (this.isWholeInView(testedIndex, scrollIndex, viewportSize)) {
|
|
||||||
return testedIndex;
|
|
||||||
}
|
|
||||||
testedIndex++;
|
|
||||||
}
|
|
||||||
return this.scrollCount - 1;
|
|
||||||
|
|
||||||
// let res: number = 0;
|
|
||||||
// let testedIndex: number = scrollIndex;
|
|
||||||
// while (res < viewportSize && testedIndex >= 0) {
|
|
||||||
// let size: number = this.getSizeByScrollIndex(testedIndex);
|
|
||||||
// if (res + size > viewportSize) return testedIndex + 1;
|
|
||||||
// testedIndex--;
|
|
||||||
// res += size;
|
|
||||||
// }
|
|
||||||
// if (res >= viewportSize && testedIndex < scrollIndex) return testedIndex + 1;
|
|
||||||
// return firstVisibleIndex;
|
|
||||||
}
|
|
||||||
public resize(realIndex: number, newSize: number): void {
|
|
||||||
if (realIndex < 0) return;
|
|
||||||
let modelIndex: number = this.realToModel(realIndex);
|
|
||||||
if (modelIndex < 0) return;
|
|
||||||
this.sizeOverridesByModelIndex[modelIndex] = newSize;
|
|
||||||
this.buildIndex();
|
|
||||||
}
|
|
||||||
public setExtraordinaryIndexes(hidden: number[], frozen: number[]): void {
|
|
||||||
//this._hiddenAndFrozenModelIndexes = _.clone(hidden);
|
|
||||||
hidden = hidden.filter(x => x >= 0);
|
|
||||||
frozen = frozen.filter(x => x >= 0);
|
|
||||||
|
|
||||||
hidden.sort((a, b) => a - b);
|
|
||||||
frozen.sort((a, b) => a - b);
|
|
||||||
this.frozenModelIndexes = _.filter(frozen, x => !_.includes(hidden, x));
|
|
||||||
this.hiddenModelIndexes = _.filter(hidden, x => !_.includes(frozen, x));
|
|
||||||
this.hiddenAndFrozenModelIndexes = _.concat(hidden, this.frozenModelIndexes);
|
|
||||||
this.frozenModelIndexes.sort((a, b) => a - b);
|
|
||||||
if (this.hiddenAndFrozenModelIndexes.length == 0) this.hiddenAndFrozenModelIndexes = null;
|
|
||||||
if (this.frozenModelIndexes.length == 0) this.frozenModelIndexes = null;
|
|
||||||
this.buildIndex();
|
|
||||||
}
|
|
||||||
public realToModel(realIndex: number): number {
|
|
||||||
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return realIndex;
|
|
||||||
if (realIndex < 0) return -1;
|
|
||||||
if (realIndex < this.frozenCount && this.frozenModelIndexes != null) return this.frozenModelIndexes[realIndex];
|
|
||||||
if (this.hiddenAndFrozenModelIndexes == null) return realIndex;
|
|
||||||
realIndex -= this.frozenCount;
|
|
||||||
for (let hidItem of this.hiddenAndFrozenModelIndexes) {
|
|
||||||
if (realIndex < hidItem) return realIndex;
|
|
||||||
realIndex++;
|
|
||||||
}
|
|
||||||
return realIndex;
|
|
||||||
}
|
|
||||||
public modelToReal(modelIndex: number): number {
|
|
||||||
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return modelIndex;
|
|
||||||
if (modelIndex < 0) return -1;
|
|
||||||
let frozenIndex: number = this.frozenModelIndexes != null ? _.indexOf(this.frozenModelIndexes, modelIndex) : -1;
|
|
||||||
if (frozenIndex >= 0) return frozenIndex;
|
|
||||||
if (this.hiddenAndFrozenModelIndexes == null) return modelIndex;
|
|
||||||
let hiddenIndex: number = _.sortedIndex(this.hiddenAndFrozenModelIndexes, modelIndex);
|
|
||||||
if (this.hiddenAndFrozenModelIndexes[hiddenIndex] == modelIndex) return -1;
|
|
||||||
if (hiddenIndex >= 0) return modelIndex - hiddenIndex + this.frozenCount;
|
|
||||||
return modelIndex;
|
|
||||||
}
|
|
||||||
public getFrozenPosition(frozenIndex: number): number {
|
|
||||||
return this.frozenItems[frozenIndex].position;
|
|
||||||
}
|
|
||||||
public hasSizeOverride(modelIndex: number): boolean {
|
|
||||||
return _.has(this.sizeOverridesByModelIndex, modelIndex);
|
|
||||||
}
|
|
||||||
public isVisible(testedRealIndex: number, firstVisibleScrollIndex: number, viewportSize: number): boolean {
|
|
||||||
if (testedRealIndex < 0) return false;
|
|
||||||
if (testedRealIndex >= 0 && testedRealIndex < this.frozenCount) return true;
|
|
||||||
let scrollIndex: number = testedRealIndex - this.frozenCount;
|
|
||||||
let onPageIndex: number = scrollIndex - firstVisibleScrollIndex;
|
|
||||||
return onPageIndex >= 0 && onPageIndex < this.getVisibleScrollCount(firstVisibleScrollIndex, viewportSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import { useSetOpenedTabs } from '../utility/globalState';
|
|
||||||
import DataGridCore from './DataGridCore';
|
|
||||||
import useSocket from '../utility/SocketProvider';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import ImportExportModal from '../modals/ImportExportModal';
|
|
||||||
import { changeSetToSql, createChangeSet, getChangeSetInsertedRows } from 'dbgate-datalib';
|
|
||||||
import LoadingDataGridCore from './LoadingDataGridCore';
|
|
||||||
import ChangeSetGrider from './ChangeSetGrider';
|
|
||||||
import { scriptToSql } from 'dbgate-sqltree';
|
|
||||||
import useModalState from '../modals/useModalState';
|
|
||||||
import ConfirmSqlModal from '../modals/ConfirmSqlModal';
|
|
||||||
import ErrorMessageModal from '../modals/ErrorMessageModal';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
|
|
||||||
/** @param props {import('./types').DataGridProps} */
|
|
||||||
async function loadDataPage(props, offset, limit) {
|
|
||||||
const { display, conid, database } = props;
|
|
||||||
|
|
||||||
const sql = display.getPageQuery(offset, limit);
|
|
||||||
|
|
||||||
const response = await axios.request({
|
|
||||||
url: 'database-connections/query-data',
|
|
||||||
method: 'post',
|
|
||||||
params: {
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
},
|
|
||||||
data: { sql },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.errorMessage) return response.data;
|
|
||||||
return response.data.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dataPageAvailable(props) {
|
|
||||||
const { display } = props;
|
|
||||||
const sql = display.getPageQuery(0, 1);
|
|
||||||
return !!sql;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRowCount(props) {
|
|
||||||
const { display, conid, database } = props;
|
|
||||||
|
|
||||||
const sql = display.getCountQuery();
|
|
||||||
|
|
||||||
const response = await axios.request({
|
|
||||||
url: 'database-connections/query-data',
|
|
||||||
method: 'post',
|
|
||||||
params: {
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
},
|
|
||||||
data: { sql },
|
|
||||||
});
|
|
||||||
|
|
||||||
return parseInt(response.data.rows[0].count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param props {import('./types').DataGridProps} */
|
|
||||||
export default function SqlDataGridCore(props) {
|
|
||||||
const { conid, database, display, changeSetState, dispatchChangeSet } = props;
|
|
||||||
const showModal = useShowModal();
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
const confirmSqlModalState = useModalState();
|
|
||||||
const [confirmSql, setConfirmSql] = React.useState('');
|
|
||||||
|
|
||||||
const changeSet = changeSetState && changeSetState.value;
|
|
||||||
const changeSetRef = React.useRef(changeSet);
|
|
||||||
changeSetRef.current = changeSet;
|
|
||||||
|
|
||||||
function exportGrid() {
|
|
||||||
const initialValues = {};
|
|
||||||
initialValues.sourceStorageType = 'query';
|
|
||||||
initialValues.sourceConnectionId = conid;
|
|
||||||
initialValues.sourceDatabaseName = database;
|
|
||||||
initialValues.sourceSql = display.getExportQuery();
|
|
||||||
initialValues.sourceList = display.baseTable ? [display.baseTable.pureName] : [];
|
|
||||||
showModal(modalState => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
|
|
||||||
}
|
|
||||||
function openActiveChart() {
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: 'Chart #',
|
|
||||||
icon: 'img chart',
|
|
||||||
tabComponent: 'ChartTab',
|
|
||||||
props: {
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
editor: {
|
|
||||||
config: { chartType: 'bar' },
|
|
||||||
sql: display.getExportQuery(select => {
|
|
||||||
select.orderBy = null;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function openQuery() {
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: 'Query #',
|
|
||||||
icon: 'img sql-file',
|
|
||||||
tabComponent: 'QueryTab',
|
|
||||||
props: {
|
|
||||||
schemaName: display.baseTable.schemaName,
|
|
||||||
pureName: display.baseTable.pureName,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
editor: display.getExportQuery(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSave() {
|
|
||||||
const script = changeSetToSql(changeSetRef.current, display.dbinfo);
|
|
||||||
const sql = scriptToSql(display.driver, script);
|
|
||||||
setConfirmSql(sql);
|
|
||||||
confirmSqlModalState.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleConfirmSql() {
|
|
||||||
const resp = await axios.request({
|
|
||||||
url: 'database-connections/query-data',
|
|
||||||
method: 'post',
|
|
||||||
params: {
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
},
|
|
||||||
data: { sql: confirmSql },
|
|
||||||
});
|
|
||||||
const { errorMessage } = resp.data || {};
|
|
||||||
if (errorMessage) {
|
|
||||||
showModal(modalState => (
|
|
||||||
<ErrorMessageModal modalState={modalState} message={errorMessage} title="Error when saving" />
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
|
||||||
setConfirmSql(null);
|
|
||||||
display.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// const grider = React.useMemo(()=>new ChangeSetGrider())
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LoadingDataGridCore
|
|
||||||
{...props}
|
|
||||||
exportGrid={exportGrid}
|
|
||||||
openActiveChart={openActiveChart}
|
|
||||||
openQuery={openQuery}
|
|
||||||
loadDataPage={loadDataPage}
|
|
||||||
dataPageAvailable={dataPageAvailable}
|
|
||||||
loadRowCount={loadRowCount}
|
|
||||||
griderFactory={ChangeSetGrider.factory}
|
|
||||||
griderFactoryDeps={ChangeSetGrider.factoryDeps}
|
|
||||||
// changeSet={changeSetState && changeSetState.value}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
|
||||||
<ConfirmSqlModal
|
|
||||||
modalState={confirmSqlModalState}
|
|
||||||
sql={confirmSql}
|
|
||||||
engine={display.engine}
|
|
||||||
onConfirm={handleConfirmSql}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import DataGrid from './DataGrid';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { TableGridDisplay, TableFormViewDisplay, createGridConfig, createGridCache } from 'dbgate-datalib';
|
|
||||||
import { getFilterValueExpression } from 'dbgate-filterparser';
|
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
|
||||||
import { useConnectionInfo, getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
|
||||||
import useSocket from '../utility/SocketProvider';
|
|
||||||
import { VerticalSplitter } from '../widgets/Splitter';
|
|
||||||
import stableStringify from 'json-stable-stringify';
|
|
||||||
import ReferenceHeader from './ReferenceHeader';
|
|
||||||
import SqlDataGridCore from './SqlDataGridCore';
|
|
||||||
import useExtensions from '../utility/useExtensions';
|
|
||||||
import SqlFormView from '../formview/SqlFormView';
|
|
||||||
|
|
||||||
const ReferenceContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ReferenceGridWrapper = styled.div`
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function TableDataGrid({
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
schemaName,
|
|
||||||
pureName,
|
|
||||||
tabVisible,
|
|
||||||
toolbarPortalRef,
|
|
||||||
changeSetState,
|
|
||||||
dispatchChangeSet,
|
|
||||||
config = undefined,
|
|
||||||
setConfig = undefined,
|
|
||||||
cache = undefined,
|
|
||||||
setCache = undefined,
|
|
||||||
masterLoadedTime = undefined,
|
|
||||||
isDetailView = false,
|
|
||||||
}) {
|
|
||||||
// const [childConfig, setChildConfig] = React.useState(createGridConfig());
|
|
||||||
const [myCache, setMyCache] = React.useState(createGridCache());
|
|
||||||
const [childCache, setChildCache] = React.useState(createGridCache());
|
|
||||||
const [refReloadToken, setRefReloadToken] = React.useState(0);
|
|
||||||
const [myLoadedTime, setMyLoadedTime] = React.useState(0);
|
|
||||||
const extensions = useExtensions();
|
|
||||||
|
|
||||||
const { childConfig } = config;
|
|
||||||
const setChildConfig = (value, reference = undefined) => {
|
|
||||||
if (_.isFunction(value)) {
|
|
||||||
setConfig(x => ({
|
|
||||||
...x,
|
|
||||||
childConfig: value(x.childConfig),
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setConfig(x => ({
|
|
||||||
...x,
|
|
||||||
childConfig: value,
|
|
||||||
reference: reference === undefined ? x.reference : reference,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const { reference } = config;
|
|
||||||
|
|
||||||
const connection = useConnectionInfo({ conid });
|
|
||||||
const dbinfo = useDatabaseInfo({ conid, database });
|
|
||||||
// const [reference, setReference] = React.useState(null);
|
|
||||||
|
|
||||||
function createDisplay() {
|
|
||||||
return connection
|
|
||||||
? new TableGridDisplay(
|
|
||||||
{ schemaName, pureName },
|
|
||||||
findEngineDriver(connection, extensions),
|
|
||||||
config,
|
|
||||||
setConfig,
|
|
||||||
cache || myCache,
|
|
||||||
setCache || setMyCache,
|
|
||||||
dbinfo
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFormDisplay() {
|
|
||||||
return connection
|
|
||||||
? new TableFormViewDisplay(
|
|
||||||
{ schemaName, pureName },
|
|
||||||
findEngineDriver(connection, extensions),
|
|
||||||
config,
|
|
||||||
setConfig,
|
|
||||||
cache || myCache,
|
|
||||||
setCache || setMyCache,
|
|
||||||
dbinfo
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [display, setDisplay] = React.useState(createDisplay());
|
|
||||||
const [formDisplay, setFormDisplay] = React.useState(createFormDisplay());
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setRefReloadToken(v => v + 1);
|
|
||||||
if (!reference && display && display.isGrouped) display.clearGrouping();
|
|
||||||
}, [reference]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const newDisplay = createDisplay();
|
|
||||||
if (!newDisplay) return;
|
|
||||||
if (display && display.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;
|
|
||||||
setDisplay(newDisplay);
|
|
||||||
}, [connection, config, cache || myCache, conid, database, schemaName, pureName, dbinfo, extensions]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const newDisplay = createFormDisplay();
|
|
||||||
if (!newDisplay) return;
|
|
||||||
if (formDisplay && formDisplay.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;
|
|
||||||
setFormDisplay(newDisplay);
|
|
||||||
}, [connection, config, cache || myCache, conid, database, schemaName, pureName, dbinfo, extensions]);
|
|
||||||
|
|
||||||
const handleDatabaseStructureChanged = React.useCallback(() => {
|
|
||||||
(setCache || setMyCache)(createGridCache());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const socket = useSocket();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (display && !display.isLoadedCorrectly) {
|
|
||||||
if (conid && socket) {
|
|
||||||
socket.on(`database-structure-changed-${conid}-${database}`, handleDatabaseStructureChanged);
|
|
||||||
return () => {
|
|
||||||
socket.off(`database-structure-changed-${conid}-${database}`, handleDatabaseStructureChanged);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [conid, database, display]);
|
|
||||||
|
|
||||||
const handleReferenceSourceChanged = React.useCallback(
|
|
||||||
(selectedRows, loadedTime) => {
|
|
||||||
setMyLoadedTime(loadedTime);
|
|
||||||
if (!reference) return;
|
|
||||||
|
|
||||||
const filtersBase = display && display.isGrouped ? config.filters : childConfig.filters;
|
|
||||||
|
|
||||||
const filters = {
|
|
||||||
...filtersBase,
|
|
||||||
..._.fromPairs(
|
|
||||||
reference.columns.map(col => [
|
|
||||||
col.refName,
|
|
||||||
selectedRows.map(x => getFilterValueExpression(x[col.baseName], col.dataType)).join(', '),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
};
|
|
||||||
if (stableStringify(filters) != stableStringify(childConfig.filters)) {
|
|
||||||
setChildConfig(cfg => ({
|
|
||||||
...cfg,
|
|
||||||
filters,
|
|
||||||
}));
|
|
||||||
setChildCache(ca => ({
|
|
||||||
...ca,
|
|
||||||
refreshTime: new Date().getTime(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[childConfig, reference]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCloseReference = () => {
|
|
||||||
setChildConfig(null, null);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!display) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VerticalSplitter>
|
|
||||||
<DataGrid
|
|
||||||
// key={`${conid}, ${database}, ${schemaName}, ${pureName}`}
|
|
||||||
config={config}
|
|
||||||
setConfig={setConfig}
|
|
||||||
conid={conid}
|
|
||||||
database={database}
|
|
||||||
display={display}
|
|
||||||
formDisplay={formDisplay}
|
|
||||||
tabVisible={tabVisible}
|
|
||||||
changeSetState={changeSetState}
|
|
||||||
dispatchChangeSet={dispatchChangeSet}
|
|
||||||
toolbarPortalRef={toolbarPortalRef}
|
|
||||||
showReferences
|
|
||||||
onReferenceClick={reference => setChildConfig(createGridConfig(), reference)}
|
|
||||||
onReferenceSourceChanged={reference ? handleReferenceSourceChanged : null}
|
|
||||||
refReloadToken={refReloadToken.toString()}
|
|
||||||
masterLoadedTime={masterLoadedTime}
|
|
||||||
GridCore={SqlDataGridCore}
|
|
||||||
FormView={SqlFormView}
|
|
||||||
isDetailView={isDetailView}
|
|
||||||
// tableInfo={
|
|
||||||
// dbinfo && dbinfo.tables && dbinfo.tables.find((x) => x.pureName == pureName && x.schemaName == schemaName)
|
|
||||||
// }
|
|
||||||
/>
|
|
||||||
{reference && (
|
|
||||||
<ReferenceContainer>
|
|
||||||
<ReferenceHeader reference={reference} onClose={handleCloseReference} />
|
|
||||||
<ReferenceGridWrapper>
|
|
||||||
<TableDataGrid
|
|
||||||
key={`${reference.schemaName}.${reference.pureName}`}
|
|
||||||
conid={conid}
|
|
||||||
database={database}
|
|
||||||
pureName={reference.pureName}
|
|
||||||
schemaName={reference.schemaName}
|
|
||||||
changeSetState={changeSetState}
|
|
||||||
dispatchChangeSet={dispatchChangeSet}
|
|
||||||
toolbarPortalRef={toolbarPortalRef}
|
|
||||||
tabVisible={false}
|
|
||||||
config={childConfig}
|
|
||||||
setConfig={setChildConfig}
|
|
||||||
cache={childCache}
|
|
||||||
setCache={setChildCache}
|
|
||||||
masterLoadedTime={myLoadedTime}
|
|
||||||
isDetailView
|
|
||||||
/>
|
|
||||||
</ReferenceGridWrapper>
|
|
||||||
</ReferenceContainer>
|
|
||||||
)}
|
|
||||||
</VerticalSplitter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { SeriesSizes } from './SeriesSizes';
|
|
||||||
import { CellAddress } from './selection';
|
|
||||||
import { GridDisplay } from 'dbgate-datalib';
|
|
||||||
import Grider from './Grider';
|
|
||||||
|
|
||||||
export function countColumnSizes(grider: Grider, columns, containerWidth, display: GridDisplay) {
|
|
||||||
const columnSizes = new SeriesSizes();
|
|
||||||
if (!grider || !columns) return columnSizes;
|
|
||||||
|
|
||||||
let canvas = document.createElement('canvas');
|
|
||||||
let context = canvas.getContext('2d');
|
|
||||||
|
|
||||||
//return this.context.measureText(txt).width;
|
|
||||||
|
|
||||||
// console.log('countColumnSizes', loadedRows.length, containerWidth);
|
|
||||||
|
|
||||||
columnSizes.maxSize = (containerWidth * 2) / 3;
|
|
||||||
columnSizes.count = columns.length;
|
|
||||||
|
|
||||||
// columnSizes.setExtraordinaryIndexes(this.getHiddenColumnIndexes(), this.getFrozenColumnIndexes());
|
|
||||||
// console.log('display.hiddenColumnIndexes', display.hiddenColumnIndexes)
|
|
||||||
|
|
||||||
columnSizes.setExtraordinaryIndexes(display.hiddenColumnIndexes, []);
|
|
||||||
|
|
||||||
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
|
|
||||||
//this.columnSizes.PutSizeOverride(col, this.columns[col].Name.length * 8);
|
|
||||||
const column = columns[colIndex];
|
|
||||||
|
|
||||||
if (display.config.columnWidths[column.uniqueName]) {
|
|
||||||
columnSizes.putSizeOverride(colIndex, display.config.columnWidths[column.uniqueName]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (column.columnClientObject != null && column.columnClientObject.notNull) context.font = "bold 14px Helvetica";
|
|
||||||
// else context.font = "14px Helvetica";
|
|
||||||
context.font = 'bold 14px Helvetica';
|
|
||||||
|
|
||||||
const text = column.headerText;
|
|
||||||
const headerWidth = context.measureText(text).width + 64;
|
|
||||||
|
|
||||||
// if (column.columnClientObject != null && column.columnClientObject.icon != null) headerWidth += 16;
|
|
||||||
// if (this.getFilterOnColumn(column.uniquePath)) headerWidth += 16;
|
|
||||||
// if (this.getSortOrder(column.uniquePath)) headerWidth += 16;
|
|
||||||
|
|
||||||
columnSizes.putSizeOverride(colIndex, headerWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// let headerWidth = this.rowHeaderWidthDefault;
|
|
||||||
// if (this.rowCount) headerWidth = context.measureText(this.rowCount.toString()).width + 8;
|
|
||||||
// this.rowHeaderWidth = this.rowHeaderWidthDefault;
|
|
||||||
// if (headerWidth > this.rowHeaderWidth) this.rowHeaderWidth = headerWidth;
|
|
||||||
|
|
||||||
context.font = '14px Helvetica';
|
|
||||||
for (let rowIndex = 0; rowIndex < Math.min(grider.rowCount, 20); rowIndex += 1) {
|
|
||||||
const row = grider.getRowData(rowIndex);
|
|
||||||
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
|
|
||||||
const uqName = columns[colIndex].uniqueName;
|
|
||||||
|
|
||||||
if (display.config.columnWidths[uqName]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = row[uqName];
|
|
||||||
const width = context.measureText(text).width + 8;
|
|
||||||
// console.log('colName', colName, text, width);
|
|
||||||
columnSizes.putSizeOverride(colIndex, width);
|
|
||||||
// let colName = this.columns[colIndex].uniquePath;
|
|
||||||
// let text: string = row[colName].gridText;
|
|
||||||
// let width = context.measureText(text).width + 8;
|
|
||||||
// if (row[colName].dataPrefix) width += context.measureText(row[colName].dataPrefix).width + 3;
|
|
||||||
// this.columnSizes.putSizeOverride(colIndex, width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for (let modelIndex = 0; modelIndex < this.columns.length; modelIndex++) {
|
|
||||||
// let width = getHashValue(this.widthHashPrefix + this.columns[modelIndex].uniquePath);
|
|
||||||
// if (width) this.columnSizes.putSizeOverride(modelIndex, _.toNumber(width), true);
|
|
||||||
// }
|
|
||||||
|
|
||||||
columnSizes.buildIndex();
|
|
||||||
return columnSizes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function countVisibleRealColumns(columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns) {
|
|
||||||
const visibleColumnCount = columnSizes.getVisibleScrollCount(firstVisibleColumnScrollIndex, gridScrollAreaWidth);
|
|
||||||
// console.log('visibleColumnCount', visibleColumnCount);
|
|
||||||
// console.log('gridScrollAreaWidth', gridScrollAreaWidth);
|
|
||||||
|
|
||||||
const visibleRealColumnIndexes = [];
|
|
||||||
const modelIndexes = {};
|
|
||||||
/** @type {(import('dbgate-datalib').DisplayColumn & {widthPx: string; colIndex: number})[]} */
|
|
||||||
const realColumns = [];
|
|
||||||
|
|
||||||
// frozen columns
|
|
||||||
for (let colIndex = 0; colIndex < columnSizes.frozenCount; colIndex++) {
|
|
||||||
visibleRealColumnIndexes.push(colIndex);
|
|
||||||
}
|
|
||||||
// scroll columns
|
|
||||||
for (
|
|
||||||
let colIndex = firstVisibleColumnScrollIndex;
|
|
||||||
colIndex < firstVisibleColumnScrollIndex + visibleColumnCount;
|
|
||||||
colIndex++
|
|
||||||
) {
|
|
||||||
visibleRealColumnIndexes.push(colIndex + columnSizes.frozenCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// real columns
|
|
||||||
for (let colIndex of visibleRealColumnIndexes) {
|
|
||||||
let modelColumnIndex = columnSizes.realToModel(colIndex);
|
|
||||||
modelIndexes[colIndex] = modelColumnIndex;
|
|
||||||
|
|
||||||
let col = columns[modelColumnIndex];
|
|
||||||
if (!col) continue;
|
|
||||||
const widthNumber = columnSizes.getSizeByRealIndex(colIndex);
|
|
||||||
realColumns.push({
|
|
||||||
...col,
|
|
||||||
colIndex,
|
|
||||||
widthNumber,
|
|
||||||
widthPx: `${widthNumber}px`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return realColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterCellForRow(cell, row: number): CellAddress | null {
|
|
||||||
return cell && (cell[0] == row || _.isString(cell[0])) ? cell : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterCellsForRow(cells, row: number): CellAddress[] | null {
|
|
||||||
const res = (cells || []).filter(x => x[0] == row || _.isString(x[0]));
|
|
||||||
return res.length > 0 ? res : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cellIsSelected(row, col, selectedCells) {
|
|
||||||
if (!selectedCells) return false;
|
|
||||||
for (const [selectedRow, selectedCol] of selectedCells) {
|
|
||||||
if (row == selectedRow && col == selectedCol) return true;
|
|
||||||
if (selectedRow == 'header' && col == selectedCol) return true;
|
|
||||||
if (row == selectedRow && selectedCol == 'header') return true;
|
|
||||||
if (selectedRow == 'header' && selectedCol == 'header') return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
export type CellAddress = [number | 'header' | 'filter' | undefined, number | 'header' | undefined];
|
|
||||||
export type RegularCellAddress = [number, number];
|
|
||||||
|
|
||||||
export const topLeftCell: CellAddress = [0, 0];
|
|
||||||
export const undefinedCell: CellAddress = [undefined, undefined];
|
|
||||||
export const nullCell: CellAddress = null;
|
|
||||||
export const emptyCellArray: CellAddress[] = [];
|
|
||||||
|
|
||||||
export function isRegularCell(cell: CellAddress): cell is RegularCellAddress {
|
|
||||||
if (!cell) return false;
|
|
||||||
const [row, col] = cell;
|
|
||||||
return _.isNumber(row) && _.isNumber(col);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCellRange(a: CellAddress, b: CellAddress): CellAddress[] {
|
|
||||||
const [rowA, colA] = a;
|
|
||||||
const [rowB, colB] = b;
|
|
||||||
|
|
||||||
if (_.isNumber(rowA) && _.isNumber(colA) && _.isNumber(rowB) && _.isNumber(colB)) {
|
|
||||||
const rowMin = Math.min(rowA, rowB);
|
|
||||||
const rowMax = Math.max(rowA, rowB);
|
|
||||||
const colMin = Math.min(colA, colB);
|
|
||||||
const colMax = Math.max(colA, colB);
|
|
||||||
const res = [];
|
|
||||||
for (let row = rowMin; row <= rowMax; row++) {
|
|
||||||
for (let col = colMin; col <= colMax; col++) {
|
|
||||||
res.push([row, col]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
if (rowA == 'header' && rowB == 'header' && _.isNumber(colA) && _.isNumber(colB)) {
|
|
||||||
const colMin = Math.min(colA, colB);
|
|
||||||
const colMax = Math.max(colA, colB);
|
|
||||||
const res = [];
|
|
||||||
for (let col = colMin; col <= colMax; col++) {
|
|
||||||
res.push(['header', col]);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
if (colA == 'header' && colB == 'header' && _.isNumber(rowA) && _.isNumber(rowB)) {
|
|
||||||
const rowMin = Math.min(rowA, rowB);
|
|
||||||
const rowMax = Math.max(rowA, rowB);
|
|
||||||
const res = [];
|
|
||||||
for (let row = rowMin; row <= rowMax; row++) {
|
|
||||||
res.push([row, 'header']);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
if (colA == 'header' && colB == 'header' && rowA == 'header' && rowB == 'header') {
|
|
||||||
return [['header', 'header']];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertCellAddress(row, col): CellAddress {
|
|
||||||
const rowNumber = parseInt(row);
|
|
||||||
const colNumber = parseInt(col);
|
|
||||||
return [_.isNaN(rowNumber) ? row : rowNumber, _.isNaN(colNumber) ? col : colNumber];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cellFromEvent(event): CellAddress {
|
|
||||||
const cell = event.target.closest('td');
|
|
||||||
if (!cell) return undefinedCell;
|
|
||||||
const col = cell.getAttribute('data-col');
|
|
||||||
const row = cell.getAttribute('data-row');
|
|
||||||
return convertCellAddress(row, col);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { GridDisplay, ChangeSet, GridReferenceDefinition } from 'dbgate-datalib';
|
|
||||||
import Grider from './Grider';
|
|
||||||
|
|
||||||
export interface DataGridProps {
|
|
||||||
display: GridDisplay;
|
|
||||||
tabVisible?: boolean;
|
|
||||||
changeSetState?: { value: ChangeSet };
|
|
||||||
dispatchChangeSet?: Function;
|
|
||||||
toolbarPortalRef?: any;
|
|
||||||
showReferences?: boolean;
|
|
||||||
onReferenceClick?: (def: GridReferenceDefinition) => void;
|
|
||||||
onReferenceSourceChanged?: Function;
|
|
||||||
refReloadToken?: string;
|
|
||||||
masterLoadedTime?: number;
|
|
||||||
managerSize?: number;
|
|
||||||
grider?: Grider;
|
|
||||||
conid?: string;
|
|
||||||
database?: string;
|
|
||||||
jslid?: string;
|
|
||||||
|
|
||||||
[field: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// export interface DataGridCoreProps extends DataGridProps {
|
|
||||||
// rows: any[];
|
|
||||||
// loadNextData?: Function;
|
|
||||||
// exportGrid?: Function;
|
|
||||||
// openQuery?: Function;
|
|
||||||
// undo?: Function;
|
|
||||||
// redo?: Function;
|
|
||||||
|
|
||||||
// errorMessage?: string;
|
|
||||||
// isLoadedAll?: boolean;
|
|
||||||
// loadedTime?: any;
|
|
||||||
// allRowCount?: number;
|
|
||||||
// conid?: string;
|
|
||||||
// database?: string;
|
|
||||||
// insertedRowCount?: number;
|
|
||||||
// isLoading?: boolean;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface LoadingDataGridProps extends DataGridProps {
|
|
||||||
// conid?: string;
|
|
||||||
// database?: string;
|
|
||||||
// jslid?: string;
|
|
||||||
// }
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import DesignerTable from './DesignerTable';
|
|
||||||
import uuidv1 from 'uuid/v1';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import DesignerReference from './DesignerReference';
|
|
||||||
import cleanupDesignColumns from './cleanupDesignColumns';
|
|
||||||
import { isConnectedByReference } from './designerTools';
|
|
||||||
import { getTableInfo } from '../utility/metadataLoaders';
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
background-color: ${props => props.theme.designer_background};
|
|
||||||
overflow: scroll;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Canvas = styled.div`
|
|
||||||
width: 3000px;
|
|
||||||
height: 3000px;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const EmptyInfo = styled.div`
|
|
||||||
margin: 50px;
|
|
||||||
font-size: 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function fixPositions(tables) {
|
|
||||||
const minLeft = _.min(tables.map(x => x.left));
|
|
||||||
const minTop = _.min(tables.map(x => x.top));
|
|
||||||
if (minLeft < 0 || minTop < 0) {
|
|
||||||
const dLeft = minLeft < 0 ? -minLeft : 0;
|
|
||||||
const dTop = minTop < 0 ? -minTop : 0;
|
|
||||||
return tables.map(tbl => ({
|
|
||||||
...tbl,
|
|
||||||
left: tbl.left + dLeft,
|
|
||||||
top: tbl.top + dTop,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return tables;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Designer({ value, onChange, conid, database }) {
|
|
||||||
const { tables, references } = value || {};
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const [sourceDragColumn, setSourceDragColumn] = React.useState(null);
|
|
||||||
const [targetDragColumn, setTargetDragColumn] = React.useState(null);
|
|
||||||
const domTablesRef = React.useRef({});
|
|
||||||
const wrapperRef = React.useRef();
|
|
||||||
const [changeToken, setChangeToken] = React.useState(0);
|
|
||||||
|
|
||||||
const handleDrop = e => {
|
|
||||||
var data = e.dataTransfer.getData('app_object_drag_data');
|
|
||||||
e.preventDefault();
|
|
||||||
if (!data) return;
|
|
||||||
const rect = e.target.getBoundingClientRect();
|
|
||||||
var json = JSON.parse(data);
|
|
||||||
const { objectTypeField } = json;
|
|
||||||
if (objectTypeField != 'tables' && objectTypeField != 'views') return;
|
|
||||||
json.designerId = uuidv1();
|
|
||||||
json.left = e.clientX - rect.left;
|
|
||||||
json.top = e.clientY - rect.top;
|
|
||||||
|
|
||||||
onChange(current => {
|
|
||||||
const foreignKeys = _.compact([
|
|
||||||
...(json.foreignKeys || []).map(fk => {
|
|
||||||
const tables = ((current || {}).tables || []).filter(
|
|
||||||
tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName
|
|
||||||
);
|
|
||||||
if (tables.length == 1)
|
|
||||||
return {
|
|
||||||
...fk,
|
|
||||||
sourceId: json.designerId,
|
|
||||||
targetId: tables[0].designerId,
|
|
||||||
};
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
..._.flatten(
|
|
||||||
((current || {}).tables || []).map(tbl =>
|
|
||||||
(tbl.foreignKeys || []).map(fk => {
|
|
||||||
if (fk.refTableName == json.pureName && fk.refSchemaName == json.schemaName) {
|
|
||||||
return {
|
|
||||||
...fk,
|
|
||||||
sourceId: tbl.designerId,
|
|
||||||
targetId: json.designerId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
tables: [...((current || {}).tables || []), json],
|
|
||||||
references:
|
|
||||||
foreignKeys.length == 1
|
|
||||||
? [
|
|
||||||
...((current || {}).references || []),
|
|
||||||
{
|
|
||||||
designerId: uuidv1(),
|
|
||||||
sourceId: foreignKeys[0].sourceId,
|
|
||||||
targetId: foreignKeys[0].targetId,
|
|
||||||
joinType: 'INNER JOIN',
|
|
||||||
columns: foreignKeys[0].columns.map(col => ({
|
|
||||||
source: col.columnName,
|
|
||||||
target: col.refColumnName,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: (current || {}).references,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeTable = React.useCallback(
|
|
||||||
table => {
|
|
||||||
onChange(current => ({
|
|
||||||
...current,
|
|
||||||
tables: fixPositions((current.tables || []).map(x => (x.designerId == table.designerId ? table : x))),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const bringToFront = React.useCallback(
|
|
||||||
table => {
|
|
||||||
onChange(
|
|
||||||
current => ({
|
|
||||||
...current,
|
|
||||||
tables: [...(current.tables || []).filter(x => x.designerId != table.designerId), table],
|
|
||||||
}),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeTable = React.useCallback(
|
|
||||||
table => {
|
|
||||||
onChange(current => ({
|
|
||||||
...current,
|
|
||||||
tables: (current.tables || []).filter(x => x.designerId != table.designerId),
|
|
||||||
references: (current.references || []).filter(
|
|
||||||
x => x.sourceId != table.designerId && x.targetId != table.designerId
|
|
||||||
),
|
|
||||||
columns: (current.columns || []).filter(x => x.designerId != table.designerId),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeReference = React.useCallback(
|
|
||||||
ref => {
|
|
||||||
onChange(current => ({
|
|
||||||
...current,
|
|
||||||
references: (current.references || []).map(x => (x.designerId == ref.designerId ? ref : x)),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeReference = React.useCallback(
|
|
||||||
ref => {
|
|
||||||
onChange(current => ({
|
|
||||||
...current,
|
|
||||||
references: (current.references || []).filter(x => x.designerId != ref.designerId),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCreateReference = (source, target) => {
|
|
||||||
onChange(current => {
|
|
||||||
const existingReference = (current.references || []).find(
|
|
||||||
x =>
|
|
||||||
(x.sourceId == source.designerId && x.targetId == target.designerId) ||
|
|
||||||
(x.sourceId == target.designerId && x.targetId == source.designerId)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
references: existingReference
|
|
||||||
? current.references.map(ref =>
|
|
||||||
ref == existingReference
|
|
||||||
? {
|
|
||||||
...existingReference,
|
|
||||||
columns: [
|
|
||||||
...existingReference.columns,
|
|
||||||
existingReference.sourceId == source.designerId
|
|
||||||
? {
|
|
||||||
source: source.columnName,
|
|
||||||
target: target.columnName,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
source: target.columnName,
|
|
||||||
target: source.columnName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: ref
|
|
||||||
)
|
|
||||||
: [
|
|
||||||
...(current.references || []),
|
|
||||||
{
|
|
||||||
designerId: uuidv1(),
|
|
||||||
sourceId: source.designerId,
|
|
||||||
targetId: target.designerId,
|
|
||||||
joinType: isConnectedByReference(current, source, target, null) ? 'CROSS JOIN' : 'INNER JOIN',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
source: source.columnName,
|
|
||||||
target: target.columnName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddReferenceByColumn = async (designerId, foreignKey) => {
|
|
||||||
const toTable = await getTableInfo({
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
pureName: foreignKey.refTableName,
|
|
||||||
schemaName: foreignKey.refSchemaName,
|
|
||||||
});
|
|
||||||
const newTableDesignerId = uuidv1();
|
|
||||||
onChange(current => {
|
|
||||||
const fromTable = (current.tables || []).find(x => x.designerId == designerId);
|
|
||||||
if (!fromTable) return;
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
tables: [
|
|
||||||
...(current.tables || []),
|
|
||||||
{
|
|
||||||
...toTable,
|
|
||||||
left: fromTable.left + 300,
|
|
||||||
top: fromTable.top + 50,
|
|
||||||
designerId: newTableDesignerId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
references: [
|
|
||||||
...(current.references || []),
|
|
||||||
{
|
|
||||||
designerId: uuidv1(),
|
|
||||||
sourceId: fromTable.designerId,
|
|
||||||
targetId: newTableDesignerId,
|
|
||||||
joinType: 'INNER JOIN',
|
|
||||||
columns: foreignKey.columns.map(col => ({
|
|
||||||
source: col.columnName,
|
|
||||||
target: col.refColumnName,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectColumn = React.useCallback(
|
|
||||||
column => {
|
|
||||||
onChange(
|
|
||||||
current => ({
|
|
||||||
...current,
|
|
||||||
columns: (current.columns || []).find(
|
|
||||||
x => x.designerId == column.designerId && x.columnName == column.columnName
|
|
||||||
)
|
|
||||||
? current.columns
|
|
||||||
: [...cleanupDesignColumns(current.columns), _.pick(column, ['designerId', 'columnName'])],
|
|
||||||
}),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChangeColumn = React.useCallback(
|
|
||||||
(column, changeFunc) => {
|
|
||||||
onChange(current => {
|
|
||||||
const currentColumns = (current || {}).columns || [];
|
|
||||||
const existing = currentColumns.find(
|
|
||||||
x => x.designerId == column.designerId && x.columnName == column.columnName
|
|
||||||
);
|
|
||||||
if (existing) {
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
columns: currentColumns.map(x => (x == existing ? changeFunc(existing) : x)),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...current,
|
|
||||||
columns: [
|
|
||||||
...cleanupDesignColumns(currentColumns),
|
|
||||||
changeFunc(_.pick(column, ['designerId', 'columnName'])),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// setTimeout(() => setChangeToken((x) => x + 1), 100);
|
|
||||||
// }, [value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper theme={theme}>
|
|
||||||
{(tables || []).length == 0 && <EmptyInfo>Drag & drop tables or views from left panel here</EmptyInfo>}
|
|
||||||
<Canvas onDragOver={e => e.preventDefault()} onDrop={handleDrop} ref={wrapperRef}>
|
|
||||||
{(references || []).map(ref => (
|
|
||||||
<DesignerReference
|
|
||||||
key={ref.designerId}
|
|
||||||
changeToken={changeToken}
|
|
||||||
domTablesRef={domTablesRef}
|
|
||||||
reference={ref}
|
|
||||||
onChangeReference={changeReference}
|
|
||||||
onRemoveReference={removeReference}
|
|
||||||
designer={value}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{(tables || []).map(table => (
|
|
||||||
<DesignerTable
|
|
||||||
key={table.designerId}
|
|
||||||
sourceDragColumn={sourceDragColumn}
|
|
||||||
setSourceDragColumn={setSourceDragColumn}
|
|
||||||
targetDragColumn={targetDragColumn}
|
|
||||||
setTargetDragColumn={setTargetDragColumn}
|
|
||||||
onCreateReference={handleCreateReference}
|
|
||||||
onSelectColumn={handleSelectColumn}
|
|
||||||
onChangeColumn={handleChangeColumn}
|
|
||||||
onAddReferenceByColumn={handleAddReferenceByColumn}
|
|
||||||
table={table}
|
|
||||||
onChangeTable={changeTable}
|
|
||||||
onBringToFront={bringToFront}
|
|
||||||
onRemoveTable={removeTable}
|
|
||||||
setChangeToken={setChangeToken}
|
|
||||||
wrapperRef={wrapperRef}
|
|
||||||
onChangeDomTable={table => {
|
|
||||||
domTablesRef.current[table.designerId] = table;
|
|
||||||
}}
|
|
||||||
designer={value}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Canvas>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { dumpSqlSelect, Select, JoinType, Condition, Relation, mergeConditions, Source } from 'dbgate-sqltree';
|
|
||||||
import { EngineDriver } from 'dbgate-types';
|
|
||||||
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
|
|
||||||
import { findPrimaryTable, findConnectingReference, referenceIsJoin, referenceIsExists } from './designerTools';
|
|
||||||
|
|
||||||
export class DesignerComponent {
|
|
||||||
subComponents: DesignerComponent[] = [];
|
|
||||||
parentComponent: DesignerComponent;
|
|
||||||
parentReference: DesignerReferenceInfo;
|
|
||||||
|
|
||||||
tables: DesignerTableInfo[] = [];
|
|
||||||
nonPrimaryReferences: DesignerReferenceInfo[] = [];
|
|
||||||
|
|
||||||
get primaryTable() {
|
|
||||||
return this.tables[0];
|
|
||||||
}
|
|
||||||
get nonPrimaryTables() {
|
|
||||||
return this.tables.slice(1);
|
|
||||||
}
|
|
||||||
get nonPrimaryTablesAndReferences() {
|
|
||||||
return _.zip(this.nonPrimaryTables, this.nonPrimaryReferences);
|
|
||||||
}
|
|
||||||
get myAndParentTables() {
|
|
||||||
return [...this.parentTables, ...this.tables];
|
|
||||||
}
|
|
||||||
get parentTables() {
|
|
||||||
return this.parentComponent ? this.parentComponent.myAndParentTables : [];
|
|
||||||
}
|
|
||||||
get thisAndSubComponentsTables() {
|
|
||||||
return [...this.tables, ..._.flatten(this.subComponents.map(x => x.thisAndSubComponentsTables))];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DesignerComponentCreator {
|
|
||||||
toAdd: DesignerTableInfo[];
|
|
||||||
components: DesignerComponent[] = [];
|
|
||||||
|
|
||||||
constructor(public designer: DesignerInfo) {
|
|
||||||
this.toAdd = [...designer.tables];
|
|
||||||
while (this.toAdd.length > 0) {
|
|
||||||
const component = this.parseComponent(null);
|
|
||||||
this.components.push(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseComponent(root) {
|
|
||||||
if (root == null) {
|
|
||||||
root = findPrimaryTable(this.toAdd);
|
|
||||||
}
|
|
||||||
if (!root) return null;
|
|
||||||
_.remove(this.toAdd, x => x == root);
|
|
||||||
const res = new DesignerComponent();
|
|
||||||
res.tables.push(root);
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
let found = false;
|
|
||||||
for (const test of this.toAdd) {
|
|
||||||
const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsJoin);
|
|
||||||
if (ref) {
|
|
||||||
res.tables.push(test);
|
|
||||||
res.nonPrimaryReferences.push(ref);
|
|
||||||
_.remove(this.toAdd, x => x == test);
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
let found = false;
|
|
||||||
for (const test of this.toAdd) {
|
|
||||||
const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsExists);
|
|
||||||
if (ref) {
|
|
||||||
const subComponent = this.parseComponent(test);
|
|
||||||
res.subComponents.push(subComponent);
|
|
||||||
subComponent.parentComponent = res;
|
|
||||||
subComponent.parentReference = ref;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import {
|
|
||||||
dumpSqlSelect,
|
|
||||||
Select,
|
|
||||||
JoinType,
|
|
||||||
Condition,
|
|
||||||
Relation,
|
|
||||||
mergeConditions,
|
|
||||||
Source,
|
|
||||||
ResultField,
|
|
||||||
} from 'dbgate-sqltree';
|
|
||||||
import { EngineDriver } from 'dbgate-types';
|
|
||||||
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
|
|
||||||
import { DesignerComponent } from './DesignerComponentCreator';
|
|
||||||
import {
|
|
||||||
getReferenceConditions,
|
|
||||||
referenceIsCrossJoin,
|
|
||||||
referenceIsConnecting,
|
|
||||||
mergeSelectsFromDesigner,
|
|
||||||
findQuerySource,
|
|
||||||
findDesignerFilterType,
|
|
||||||
} from './designerTools';
|
|
||||||
import { parseFilter } from 'dbgate-filterparser';
|
|
||||||
|
|
||||||
export class DesignerQueryDumper {
|
|
||||||
constructor(public designer: DesignerInfo, public components: DesignerComponent[]) {}
|
|
||||||
|
|
||||||
get topLevelTables(): DesignerTableInfo[] {
|
|
||||||
return _.flatten(this.components.map(x => x.tables));
|
|
||||||
}
|
|
||||||
|
|
||||||
dumpComponent(component: DesignerComponent) {
|
|
||||||
const select: Select = {
|
|
||||||
commandType: 'select',
|
|
||||||
from: {
|
|
||||||
name: component.primaryTable,
|
|
||||||
alias: component.primaryTable.alias,
|
|
||||||
relations: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [table, ref] of component.nonPrimaryTablesAndReferences) {
|
|
||||||
select.from.relations.push({
|
|
||||||
name: table,
|
|
||||||
alias: table.alias,
|
|
||||||
joinType: ref.joinType as JoinType,
|
|
||||||
conditions: getReferenceConditions(ref, this.designer),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const subComponent of component.subComponents) {
|
|
||||||
const subQuery = this.dumpComponent(subComponent);
|
|
||||||
subQuery.selectAll = true;
|
|
||||||
select.where = mergeConditions(select.where, {
|
|
||||||
conditionType: subComponent.parentReference.joinType == 'WHERE NOT EXISTS' ? 'notExists' : 'exists',
|
|
||||||
subQuery,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.parentReference) {
|
|
||||||
select.where = mergeConditions(select.where, {
|
|
||||||
conditionType: 'and',
|
|
||||||
conditions: getReferenceConditions(component.parentReference, this.designer),
|
|
||||||
});
|
|
||||||
|
|
||||||
// cross join conditions in subcomponents
|
|
||||||
for (const ref of this.designer.references || []) {
|
|
||||||
if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, component.tables, component.myAndParentTables)) {
|
|
||||||
select.where = mergeConditions(select.where, {
|
|
||||||
conditionType: 'and',
|
|
||||||
conditions: getReferenceConditions(ref, this.designer),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.addConditions(select, component.tables);
|
|
||||||
}
|
|
||||||
|
|
||||||
return select;
|
|
||||||
}
|
|
||||||
|
|
||||||
addConditions(select: Select, tables: DesignerTableInfo[]) {
|
|
||||||
for (const column of this.designer.columns || []) {
|
|
||||||
if (!column.filter) continue;
|
|
||||||
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
|
|
||||||
if (!table) continue;
|
|
||||||
if (!tables.find(x => x.designerId == table.designerId)) continue;
|
|
||||||
|
|
||||||
const condition = parseFilter(column.filter, findDesignerFilterType(column, this.designer));
|
|
||||||
if (condition) {
|
|
||||||
select.where = mergeConditions(
|
|
||||||
select.where,
|
|
||||||
_.cloneDeepWith(condition, expr => {
|
|
||||||
if (expr.exprType == 'placeholder')
|
|
||||||
return {
|
|
||||||
exprType: 'column',
|
|
||||||
columnName: column.columnName,
|
|
||||||
source: findQuerySource(this.designer, column.designerId),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addGroupConditions(select: Select, tables: DesignerTableInfo[], selectIsGrouped: boolean) {
|
|
||||||
for (const column of this.designer.columns || []) {
|
|
||||||
if (!column.groupFilter) continue;
|
|
||||||
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
|
|
||||||
if (!table) continue;
|
|
||||||
if (!tables.find(x => x.designerId == table.designerId)) continue;
|
|
||||||
|
|
||||||
const condition = parseFilter(column.groupFilter, findDesignerFilterType(column, this.designer));
|
|
||||||
if (condition) {
|
|
||||||
select.having = mergeConditions(
|
|
||||||
select.having,
|
|
||||||
_.cloneDeepWith(condition, expr => {
|
|
||||||
if (expr.exprType == 'placeholder') {
|
|
||||||
return this.getColumnOutputExpression(column, selectIsGrouped);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumnOutputExpression(col, selectIsGrouped): ResultField {
|
|
||||||
const source = findQuerySource(this.designer, col.designerId);
|
|
||||||
const { columnName } = col;
|
|
||||||
let { alias } = col;
|
|
||||||
if (selectIsGrouped && !col.isGrouped) {
|
|
||||||
// use aggregate
|
|
||||||
const aggregate = col.aggregate == null || col.aggregate == '---' ? 'MAX' : col.aggregate;
|
|
||||||
if (!alias) alias = `${aggregate}(${columnName})`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
exprType: 'call',
|
|
||||||
func: aggregate == 'COUNT DISTINCT' ? 'COUNT' : aggregate,
|
|
||||||
argsPrefix: aggregate == 'COUNT DISTINCT' ? 'DISTINCT' : null,
|
|
||||||
alias,
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
exprType: 'column',
|
|
||||||
columnName,
|
|
||||||
source,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
exprType: 'column',
|
|
||||||
columnName,
|
|
||||||
alias,
|
|
||||||
source,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run() {
|
|
||||||
let res: Select = null;
|
|
||||||
for (const component of this.components) {
|
|
||||||
const select = this.dumpComponent(component);
|
|
||||||
if (res == null) res = select;
|
|
||||||
else res = mergeSelectsFromDesigner(res, select);
|
|
||||||
}
|
|
||||||
|
|
||||||
// top level cross join conditions
|
|
||||||
const topLevelTables = this.topLevelTables;
|
|
||||||
for (const ref of this.designer.references || []) {
|
|
||||||
if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, topLevelTables, topLevelTables)) {
|
|
||||||
res.where = mergeConditions(res.where, {
|
|
||||||
conditionType: 'and',
|
|
||||||
conditions: getReferenceConditions(ref, this.designer),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const topLevelColumns = (this.designer.columns || []).filter(col =>
|
|
||||||
topLevelTables.find(tbl => tbl.designerId == col.designerId)
|
|
||||||
);
|
|
||||||
const selectIsGrouped = !!topLevelColumns.find(x => x.isGrouped || (x.aggregate && x.aggregate != '---'));
|
|
||||||
const outputColumns = topLevelColumns.filter(x => x.isOutput);
|
|
||||||
if (outputColumns.length == 0) {
|
|
||||||
res.selectAll = true;
|
|
||||||
} else {
|
|
||||||
res.columns = outputColumns.map(col => this.getColumnOutputExpression(col, selectIsGrouped));
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedColumns = topLevelColumns.filter(x => x.isGrouped);
|
|
||||||
if (groupedColumns.length > 0) {
|
|
||||||
res.groupBy = groupedColumns.map(col => ({
|
|
||||||
exprType: 'column',
|
|
||||||
columnName: col.columnName,
|
|
||||||
source: findQuerySource(this.designer, col.designerId),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderColumns = _.sortBy(
|
|
||||||
topLevelColumns.filter(x => x.sortOrder),
|
|
||||||
x => Math.abs(x.sortOrder)
|
|
||||||
);
|
|
||||||
if (orderColumns.length > 0) {
|
|
||||||
res.orderBy = orderColumns.map(col => ({
|
|
||||||
exprType: 'column',
|
|
||||||
direction: col.sortOrder < 0 ? 'DESC' : 'ASC',
|
|
||||||
columnName: col.columnName,
|
|
||||||
source: findQuerySource(this.designer, col.designerId),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addConditions(res, topLevelTables);
|
|
||||||
this.addGroupConditions(res, topLevelTables, selectIsGrouped);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import DomTableRef from './DomTableRef';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import { useShowMenu } from '../modals/showMenu';
|
|
||||||
import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import { isConnectedByReference } from './designerTools';
|
|
||||||
|
|
||||||
const StyledSvg = styled.svg`
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ReferenceWrapper = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid ${props => props.theme.designer_line};
|
|
||||||
background-color: ${props => props.theme.designer_background};
|
|
||||||
z-index: 900;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ReferenceText = styled.span`
|
|
||||||
position: relative;
|
|
||||||
float: left;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 900;
|
|
||||||
white-space: nowrap;
|
|
||||||
background-color: ${props => props.theme.designer_background};
|
|
||||||
`;
|
|
||||||
|
|
||||||
function ReferenceContextMenu({ remove, setJoinType, isConnected }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={remove}>Remove</DropDownMenuItem>
|
|
||||||
{!isConnected && (
|
|
||||||
<>
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
<DropDownMenuItem onClick={() => setJoinType('INNER JOIN')}>Set INNER JOIN</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setJoinType('LEFT JOIN')}>Set LEFT JOIN</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setJoinType('RIGHT JOIN')}>Set RIGHT JOIN</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setJoinType('FULL OUTER JOIN')}>Set FULL OUTER JOIN</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setJoinType('CROSS JOIN')}>Set CROSS JOIN</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setJoinType('WHERE EXISTS')}>Set WHERE EXISTS</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setJoinType('WHERE NOT EXISTS')}>Set WHERE NOT EXISTS</DropDownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DesignerReference({
|
|
||||||
domTablesRef,
|
|
||||||
reference,
|
|
||||||
changeToken,
|
|
||||||
onRemoveReference,
|
|
||||||
onChangeReference,
|
|
||||||
designer,
|
|
||||||
}) {
|
|
||||||
const { designerId, sourceId, targetId, columns, joinType } = reference;
|
|
||||||
const theme = useTheme();
|
|
||||||
const showMenu = useShowMenu();
|
|
||||||
const domTables = domTablesRef.current;
|
|
||||||
/** @type {DomTableRef} */
|
|
||||||
const sourceTable = domTables[sourceId];
|
|
||||||
/** @type {DomTableRef} */
|
|
||||||
const targetTable = domTables[targetId];
|
|
||||||
if (!sourceTable || !targetTable) return null;
|
|
||||||
const sourceRect = sourceTable.getRect();
|
|
||||||
const targetRect = targetTable.getRect();
|
|
||||||
if (!sourceRect || !targetRect) return null;
|
|
||||||
|
|
||||||
const buswi = 10;
|
|
||||||
const extwi = 25;
|
|
||||||
|
|
||||||
const possibilities = [];
|
|
||||||
possibilities.push({ xsrc: sourceRect.left - buswi, dirsrc: -1, xdst: targetRect.left - buswi, dirdst: -1 });
|
|
||||||
possibilities.push({ xsrc: sourceRect.left - buswi, dirsrc: -1, xdst: targetRect.right + buswi, dirdst: 1 });
|
|
||||||
possibilities.push({ xsrc: sourceRect.right + buswi, dirsrc: 1, xdst: targetRect.left - buswi, dirdst: -1 });
|
|
||||||
possibilities.push({ xsrc: sourceRect.right + buswi, dirsrc: 1, xdst: targetRect.right + buswi, dirdst: 1 });
|
|
||||||
|
|
||||||
let minpos = _.minBy(possibilities, p => Math.abs(p.xsrc - p.xdst));
|
|
||||||
|
|
||||||
let srcY = _.mean(columns.map(x => sourceTable.getColumnY(x.source)));
|
|
||||||
let dstY = _.mean(columns.map(x => targetTable.getColumnY(x.target)));
|
|
||||||
|
|
||||||
if (columns.length == 0) {
|
|
||||||
srcY = sourceTable.getColumnY('');
|
|
||||||
dstY = targetTable.getColumnY('');
|
|
||||||
}
|
|
||||||
|
|
||||||
const src = { x: minpos.xsrc, y: srcY };
|
|
||||||
const dst = { x: minpos.xdst, y: dstY };
|
|
||||||
|
|
||||||
const lineStyle = { fill: 'none', stroke: theme.designer_line, strokeWidth: 2 };
|
|
||||||
|
|
||||||
const handleContextMenu = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
showMenu(
|
|
||||||
event.pageX,
|
|
||||||
event.pageY,
|
|
||||||
<ReferenceContextMenu
|
|
||||||
remove={() => onRemoveReference({ designerId })}
|
|
||||||
isConnected={isConnectedByReference(designer, { designerId: sourceId }, { designerId: targetId }, reference)}
|
|
||||||
setJoinType={joinType => {
|
|
||||||
onChangeReference({
|
|
||||||
...reference,
|
|
||||||
joinType,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StyledSvg>
|
|
||||||
<polyline
|
|
||||||
points={`
|
|
||||||
${src.x},${src.y}
|
|
||||||
${src.x + extwi * minpos.dirsrc},${src.y}
|
|
||||||
${dst.x + extwi * minpos.dirdst},${dst.y}
|
|
||||||
${dst.x},${dst.y}
|
|
||||||
`}
|
|
||||||
style={lineStyle}
|
|
||||||
/>
|
|
||||||
{columns.map((col, colIndex) => {
|
|
||||||
let y1 = sourceTable.getColumnY(col.source);
|
|
||||||
let y2 = targetTable.getColumnY(col.target);
|
|
||||||
return (
|
|
||||||
<React.Fragment key={colIndex}>
|
|
||||||
<polyline
|
|
||||||
points={`
|
|
||||||
${src.x},${src.y}
|
|
||||||
${src.x},${y1}
|
|
||||||
${src.x - buswi * minpos.dirsrc},${y1}
|
|
||||||
`}
|
|
||||||
style={lineStyle}
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
points={`
|
|
||||||
${dst.x},${dst.y}
|
|
||||||
${dst.x},${y2}
|
|
||||||
${dst.x - buswi * minpos.dirdst},${y2}
|
|
||||||
`}
|
|
||||||
style={lineStyle}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</StyledSvg>
|
|
||||||
<ReferenceWrapper
|
|
||||||
theme={theme}
|
|
||||||
style={{
|
|
||||||
left: (src.x + extwi * minpos.dirsrc + dst.x + extwi * minpos.dirdst) / 2 - 16,
|
|
||||||
top: (src.y + dst.y) / 2 - 16,
|
|
||||||
}}
|
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
>
|
|
||||||
<ReferenceText theme={theme}>
|
|
||||||
{_.snakeCase(joinType || 'CROSS JOIN')
|
|
||||||
.replace('_', '\xa0')
|
|
||||||
.replace('_', '\xa0')}
|
|
||||||
</ReferenceText>
|
|
||||||
</ReferenceWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { findForeignKeyForColumn } from 'dbgate-tools';
|
|
||||||
import ColumnLabel from '../datagrid/ColumnLabel';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import DomTableRef from './DomTableRef';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { CheckboxField } from '../utility/inputs';
|
|
||||||
import { useShowMenu } from '../modals/showMenu';
|
|
||||||
import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import InputTextModal from '../modals/InputTextModal';
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
// background-color: white;
|
|
||||||
background-color: ${props => props.theme.designtable_background};
|
|
||||||
border: 1px solid ${props => props.theme.border};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Header = styled.div`
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
padding: 2px;
|
|
||||||
background: ${props =>
|
|
||||||
// @ts-ignore
|
|
||||||
props.objectTypeField == 'views'
|
|
||||||
? props.theme.designtable_background_magenta[2]
|
|
||||||
: props.theme.designtable_background_blue[2]};
|
|
||||||
border-bottom: 1px solid ${props => props.theme.border};
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ColumnsWrapper = styled.div`
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
width: calc(100% - 10px);
|
|
||||||
padding: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HeaderLabel = styled.div``;
|
|
||||||
|
|
||||||
const CloseWrapper = styled.div`
|
|
||||||
${props =>
|
|
||||||
`
|
|
||||||
background-color: ${props.theme.toolbar_background} ;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: ${props.theme.toolbar_background2} ;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:hover {
|
|
||||||
background-color: ${props.theme.toolbar_background3};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// &:hover {
|
|
||||||
// background-color: ${(props) => props.theme.designtable_background_gold[1]};
|
|
||||||
// }
|
|
||||||
|
|
||||||
const ColumnLine = styled.div`
|
|
||||||
${props =>
|
|
||||||
// @ts-ignore
|
|
||||||
!props.isDragSource &&
|
|
||||||
// @ts-ignore
|
|
||||||
!props.isDragTarget &&
|
|
||||||
`
|
|
||||||
&:hover {
|
|
||||||
background-color: ${props.theme.designtable_background_gold[1]};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
// @ts-ignore
|
|
||||||
props.isDragSource &&
|
|
||||||
`
|
|
||||||
background-color: ${props.theme.designtable_background_cyan[2]};
|
|
||||||
`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
// @ts-ignore
|
|
||||||
props.isDragTarget &&
|
|
||||||
`
|
|
||||||
background-color: ${props.theme.designtable_background_cyan[2]};
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function TableContextMenu({ remove, setTableAlias, removeTableAlias }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={remove}>Remove</DropDownMenuItem>
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
<DropDownMenuItem onClick={setTableAlias}>Set table alias</DropDownMenuItem>
|
|
||||||
{!!removeTableAlias && <DropDownMenuItem onClick={removeTableAlias}>Remove table alias</DropDownMenuItem>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ColumnContextMenu({ setSortOrder, addReference }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={() => setSortOrder(1)}>Sort ascending</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setSortOrder(-1)}>Sort descending</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setSortOrder(0)}>Unsort</DropDownMenuItem>
|
|
||||||
{!!addReference && <DropDownMenuItem onClick={addReference}>Add reference</DropDownMenuItem>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ColumnDesignerIcons({ column, designerId, designer }) {
|
|
||||||
const designerColumn = (designer.columns || []).find(
|
|
||||||
x => x.designerId == designerId && x.columnName == column.columnName
|
|
||||||
);
|
|
||||||
if (!designerColumn) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!!designerColumn.filter && <FontIcon icon="img filter" />}
|
|
||||||
{designerColumn.sortOrder > 0 && <FontIcon icon="img sort-asc" />}
|
|
||||||
{designerColumn.sortOrder < 0 && <FontIcon icon="img sort-desc" />}
|
|
||||||
{!!designerColumn.isGrouped && <FontIcon icon="img group" />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DesignerTable({
|
|
||||||
table,
|
|
||||||
onChangeTable,
|
|
||||||
onBringToFront,
|
|
||||||
onRemoveTable,
|
|
||||||
onCreateReference,
|
|
||||||
onAddReferenceByColumn,
|
|
||||||
onSelectColumn,
|
|
||||||
onChangeColumn,
|
|
||||||
sourceDragColumn,
|
|
||||||
setSourceDragColumn,
|
|
||||||
targetDragColumn,
|
|
||||||
setTargetDragColumn,
|
|
||||||
onChangeDomTable,
|
|
||||||
wrapperRef,
|
|
||||||
setChangeToken,
|
|
||||||
designer,
|
|
||||||
}) {
|
|
||||||
const { pureName, columns, left, top, designerId, alias, objectTypeField } = table;
|
|
||||||
const [movingPosition, setMovingPosition] = React.useState(null);
|
|
||||||
const movingPositionRef = React.useRef(null);
|
|
||||||
const theme = useTheme();
|
|
||||||
const domObjectsRef = React.useRef({});
|
|
||||||
const showMenu = useShowMenu();
|
|
||||||
const showModal = useShowModal();
|
|
||||||
|
|
||||||
const moveStartXRef = React.useRef(null);
|
|
||||||
const moveStartYRef = React.useRef(null);
|
|
||||||
|
|
||||||
const handleMove = React.useCallback(e => {
|
|
||||||
let diffX = e.clientX - moveStartXRef.current;
|
|
||||||
let diffY = e.clientY - moveStartYRef.current;
|
|
||||||
moveStartXRef.current = e.clientX;
|
|
||||||
moveStartYRef.current = e.clientY;
|
|
||||||
|
|
||||||
movingPositionRef.current = {
|
|
||||||
left: (movingPositionRef.current.left || 0) + diffX,
|
|
||||||
top: (movingPositionRef.current.top || 0) + diffY,
|
|
||||||
};
|
|
||||||
setMovingPosition(movingPositionRef.current);
|
|
||||||
// setChangeToken((x) => x + 1);
|
|
||||||
changeTokenDebounced.current();
|
|
||||||
// onChangeTable(
|
|
||||||
// {
|
|
||||||
// ...props,
|
|
||||||
// left: (left || 0) + diffX,
|
|
||||||
// top: (top || 0) + diffY,
|
|
||||||
// },
|
|
||||||
// index
|
|
||||||
// );
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const changeTokenDebounced = React.useRef(
|
|
||||||
// @ts-ignore
|
|
||||||
_.debounce(() => setChangeToken(x => x + 1), 100)
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMoveEnd = React.useCallback(
|
|
||||||
e => {
|
|
||||||
if (movingPositionRef.current) {
|
|
||||||
onChangeTable({
|
|
||||||
...table,
|
|
||||||
left: movingPositionRef.current.left,
|
|
||||||
top: movingPositionRef.current.top,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
movingPositionRef.current = null;
|
|
||||||
setMovingPosition(null);
|
|
||||||
changeTokenDebounced.current();
|
|
||||||
// setChangeToken((x) => x + 1);
|
|
||||||
|
|
||||||
// this.props.model.fixPositions();
|
|
||||||
|
|
||||||
// this.props.designer.changedModel(true);
|
|
||||||
},
|
|
||||||
[onChangeTable, table]
|
|
||||||
);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (movingPosition) {
|
|
||||||
document.addEventListener('mousemove', handleMove, true);
|
|
||||||
document.addEventListener('mouseup', handleMoveEnd, true);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousemove', handleMove, true);
|
|
||||||
document.removeEventListener('mouseup', handleMoveEnd, true);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [movingPosition == null, handleMove, handleMoveEnd]);
|
|
||||||
|
|
||||||
const headerMouseDown = React.useCallback(
|
|
||||||
e => {
|
|
||||||
e.preventDefault();
|
|
||||||
moveStartXRef.current = e.clientX;
|
|
||||||
moveStartYRef.current = e.clientY;
|
|
||||||
movingPositionRef.current = { left, top };
|
|
||||||
setMovingPosition(movingPositionRef.current);
|
|
||||||
// setIsMoving(true);
|
|
||||||
},
|
|
||||||
[handleMove, handleMoveEnd]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatchDomColumn = (columnName, dom) => {
|
|
||||||
domObjectsRef.current[columnName] = dom;
|
|
||||||
onChangeDomTable(new DomTableRef(table, domObjectsRef.current, wrapperRef.current));
|
|
||||||
changeTokenDebounced.current();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetTableAlias = () => {
|
|
||||||
showModal(modalState => (
|
|
||||||
<InputTextModal
|
|
||||||
modalState={modalState}
|
|
||||||
value={alias || ''}
|
|
||||||
label="New alias"
|
|
||||||
header="Set table alias"
|
|
||||||
onConfirm={newAlias => {
|
|
||||||
onChangeTable({
|
|
||||||
...table,
|
|
||||||
alias: newAlias,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHeaderContextMenu = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
showMenu(
|
|
||||||
event.pageX,
|
|
||||||
event.pageY,
|
|
||||||
<TableContextMenu
|
|
||||||
remove={() => onRemoveTable({ designerId })}
|
|
||||||
setTableAlias={handleSetTableAlias}
|
|
||||||
removeTableAlias={
|
|
||||||
alias
|
|
||||||
? () =>
|
|
||||||
onChangeTable({
|
|
||||||
...table,
|
|
||||||
alias: null,
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleColumnContextMenu = column => event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const foreignKey = findForeignKeyForColumn(table, column);
|
|
||||||
showMenu(
|
|
||||||
event.pageX,
|
|
||||||
event.pageY,
|
|
||||||
<ColumnContextMenu
|
|
||||||
setSortOrder={sortOrder => {
|
|
||||||
onChangeColumn(
|
|
||||||
{
|
|
||||||
...column,
|
|
||||||
designerId,
|
|
||||||
},
|
|
||||||
col => ({ ...col, sortOrder })
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
addReference={
|
|
||||||
foreignKey
|
|
||||||
? () => {
|
|
||||||
onAddReferenceByColumn(designerId, foreignKey);
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
theme={theme}
|
|
||||||
style={{
|
|
||||||
left: movingPosition ? movingPosition.left : left,
|
|
||||||
top: movingPosition ? movingPosition.top : top,
|
|
||||||
}}
|
|
||||||
onMouseDown={() => onBringToFront(table)}
|
|
||||||
ref={dom => dispatchDomColumn('', dom)}
|
|
||||||
>
|
|
||||||
<Header
|
|
||||||
onMouseDown={headerMouseDown}
|
|
||||||
theme={theme}
|
|
||||||
onContextMenu={handleHeaderContextMenu}
|
|
||||||
// @ts-ignore
|
|
||||||
objectTypeField={objectTypeField}
|
|
||||||
>
|
|
||||||
<HeaderLabel>{alias || pureName}</HeaderLabel>
|
|
||||||
<CloseWrapper onClick={() => onRemoveTable(table)} theme={theme}>
|
|
||||||
<FontIcon icon="icon close" />
|
|
||||||
</CloseWrapper>
|
|
||||||
</Header>
|
|
||||||
<ColumnsWrapper>
|
|
||||||
{(columns || []).map(column => (
|
|
||||||
<ColumnLine
|
|
||||||
onContextMenu={handleColumnContextMenu(column)}
|
|
||||||
key={column.columnName}
|
|
||||||
theme={theme}
|
|
||||||
draggable
|
|
||||||
ref={dom => dispatchDomColumn(column.columnName, dom)}
|
|
||||||
// @ts-ignore
|
|
||||||
isDragSource={
|
|
||||||
sourceDragColumn &&
|
|
||||||
sourceDragColumn.designerId == designerId &&
|
|
||||||
sourceDragColumn.columnName == column.columnName
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
isDragTarget={
|
|
||||||
targetDragColumn &&
|
|
||||||
targetDragColumn.designerId == designerId &&
|
|
||||||
targetDragColumn.columnName == column.columnName
|
|
||||||
}
|
|
||||||
onDragStart={e => {
|
|
||||||
const dragData = {
|
|
||||||
...column,
|
|
||||||
designerId,
|
|
||||||
};
|
|
||||||
setSourceDragColumn(dragData);
|
|
||||||
e.dataTransfer.setData('designer_column_drag_data', JSON.stringify(dragData));
|
|
||||||
}}
|
|
||||||
onDragEnd={e => {
|
|
||||||
setTargetDragColumn(null);
|
|
||||||
setSourceDragColumn(null);
|
|
||||||
}}
|
|
||||||
onDragOver={e => {
|
|
||||||
if (sourceDragColumn) {
|
|
||||||
e.preventDefault();
|
|
||||||
setTargetDragColumn({
|
|
||||||
...column,
|
|
||||||
designerId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDrop={e => {
|
|
||||||
var data = e.dataTransfer.getData('designer_column_drag_data');
|
|
||||||
e.preventDefault();
|
|
||||||
if (!data) return;
|
|
||||||
onCreateReference(sourceDragColumn, targetDragColumn);
|
|
||||||
setTargetDragColumn(null);
|
|
||||||
setSourceDragColumn(null);
|
|
||||||
}}
|
|
||||||
onMouseDown={e =>
|
|
||||||
onSelectColumn({
|
|
||||||
...column,
|
|
||||||
designerId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CheckboxField
|
|
||||||
checked={
|
|
||||||
!!(designer.columns || []).find(
|
|
||||||
x => x.designerId == designerId && x.columnName == column.columnName && x.isOutput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onChange={e => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
onChangeColumn(
|
|
||||||
{
|
|
||||||
...column,
|
|
||||||
designerId,
|
|
||||||
},
|
|
||||||
col => ({ ...col, isOutput: true })
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
onChangeColumn(
|
|
||||||
{
|
|
||||||
...column,
|
|
||||||
designerId,
|
|
||||||
},
|
|
||||||
col => ({ ...col, isOutput: false })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ColumnLabel {...column} foreignKey={findForeignKeyForColumn(table, column)} forceIcon />
|
|
||||||
<ColumnDesignerIcons column={column} designerId={designerId} designer={designer} />
|
|
||||||
</ColumnLine>
|
|
||||||
))}
|
|
||||||
</ColumnsWrapper>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { DesignerTableInfo } from './types';
|
|
||||||
|
|
||||||
export default class DomTableRef {
|
|
||||||
domTable: Element;
|
|
||||||
domWrapper: Element;
|
|
||||||
table: DesignerTableInfo;
|
|
||||||
designerId: string;
|
|
||||||
domRefs: { [column: string]: Element };
|
|
||||||
|
|
||||||
constructor(table: DesignerTableInfo, domRefs, domWrapper: Element) {
|
|
||||||
this.domTable = domRefs[''];
|
|
||||||
this.domWrapper = domWrapper;
|
|
||||||
this.table = table;
|
|
||||||
this.designerId = table.designerId;
|
|
||||||
this.domRefs = domRefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRect() {
|
|
||||||
if (!this.domWrapper) return null;
|
|
||||||
if (!this.domTable) return null;
|
|
||||||
|
|
||||||
const wrap = this.domWrapper.getBoundingClientRect();
|
|
||||||
const rect = this.domTable.getBoundingClientRect();
|
|
||||||
return {
|
|
||||||
left: rect.left - wrap.left,
|
|
||||||
top: rect.top - wrap.top,
|
|
||||||
right: rect.right - wrap.left,
|
|
||||||
bottom: rect.bottom - wrap.top,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumnY(columnName: string) {
|
|
||||||
let col = this.domRefs[columnName];
|
|
||||||
if (!col) return null;
|
|
||||||
const rect = col.getBoundingClientRect();
|
|
||||||
const wrap = this.domWrapper.getBoundingClientRect();
|
|
||||||
return (rect.top + rect.bottom) / 2 - wrap.top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import DataFilterControl from '../datagrid/DataFilterControl';
|
|
||||||
import { CheckboxField, SelectField, TextField } from '../utility/inputs';
|
|
||||||
import TableControl, { TableColumn } from '../utility/TableControl';
|
|
||||||
import InlineButton from '../widgets/InlineButton';
|
|
||||||
import { findDesignerFilterType } from './designerTools';
|
|
||||||
|
|
||||||
function getTableDisplayName(column, tables) {
|
|
||||||
const table = (tables || []).find(x => x.designerId == column.designerId);
|
|
||||||
if (table) return table.alias || table.pureName;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function QueryDesignColumns({ value, onChange }) {
|
|
||||||
const { columns, tables } = value || {};
|
|
||||||
|
|
||||||
const changeColumn = React.useCallback(
|
|
||||||
col => {
|
|
||||||
onChange(current => ({
|
|
||||||
...current,
|
|
||||||
columns: (current.columns || []).map(x =>
|
|
||||||
x.designerId == col.designerId && x.columnName == col.columnName ? col : x
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeColumn = React.useCallback(
|
|
||||||
col => {
|
|
||||||
onChange(current => ({
|
|
||||||
...current,
|
|
||||||
columns: (current.columns || []).filter(x => x.designerId != col.designerId || x.columnName != col.columnName),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasGroupedColumn = !!(columns || []).find(x => x.isGrouped);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableControl rows={columns || []}>
|
|
||||||
<TableColumn fieldName="columnName" header="Column/Expression" />
|
|
||||||
<TableColumn fieldName="tableDisplayName" header="Table" formatter={row => getTableDisplayName(row, tables)} />
|
|
||||||
<TableColumn
|
|
||||||
fieldName="isOutput"
|
|
||||||
header="Output"
|
|
||||||
formatter={row => (
|
|
||||||
<CheckboxField
|
|
||||||
checked={row.isOutput}
|
|
||||||
onChange={e => {
|
|
||||||
if (e.target.checked) changeColumn({ ...row, isOutput: true });
|
|
||||||
else changeColumn({ ...row, isOutput: false });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<TableColumn
|
|
||||||
fieldName="alias"
|
|
||||||
header="Alias"
|
|
||||||
formatter={row => (
|
|
||||||
<TextField
|
|
||||||
value={row.alias}
|
|
||||||
onChange={e => {
|
|
||||||
changeColumn({ ...row, alias: e.target.value });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TableColumn
|
|
||||||
fieldName="isGrouped"
|
|
||||||
header="Group by"
|
|
||||||
formatter={row => (
|
|
||||||
<CheckboxField
|
|
||||||
checked={row.isGrouped}
|
|
||||||
onChange={e => {
|
|
||||||
if (e.target.checked) changeColumn({ ...row, isGrouped: true });
|
|
||||||
else changeColumn({ ...row, isGrouped: false });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<TableColumn
|
|
||||||
fieldName="aggregate"
|
|
||||||
header="Aggregate"
|
|
||||||
formatter={row =>
|
|
||||||
!row.isGrouped && (
|
|
||||||
<SelectField
|
|
||||||
value={row.aggregate}
|
|
||||||
onChange={e => {
|
|
||||||
changeColumn({ ...row, aggregate: e.target.value });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="---">---</option>
|
|
||||||
<option value="MIN">MIN</option>
|
|
||||||
<option value="MAX">MAX</option>
|
|
||||||
<option value="COUNT">COUNT</option>
|
|
||||||
<option value="COUNT DISTINCT">COUNT DISTINCT</option>
|
|
||||||
<option value="SUM">SUM</option>
|
|
||||||
<option value="AVG">AVG</option>
|
|
||||||
</SelectField>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TableColumn
|
|
||||||
fieldName="sortOrder"
|
|
||||||
header="Sort order"
|
|
||||||
formatter={row => (
|
|
||||||
<SelectField
|
|
||||||
value={row.sortOrder}
|
|
||||||
onChange={e => {
|
|
||||||
changeColumn({ ...row, sortOrder: parseInt(e.target.value) });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="0">---</option>
|
|
||||||
<option value="1">1st, ascending</option>
|
|
||||||
<option value="-1">1st, descending</option>
|
|
||||||
<option value="2">2nd, ascending</option>
|
|
||||||
<option value="-2">2nd, descending</option>
|
|
||||||
<option value="3">3rd, ascending</option>
|
|
||||||
<option value="-3">3rd, descending</option>,
|
|
||||||
</SelectField>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<TableColumn
|
|
||||||
fieldName="filter"
|
|
||||||
header="Filter"
|
|
||||||
formatter={row => (
|
|
||||||
<DataFilterControl
|
|
||||||
filterType={findDesignerFilterType(row, value)}
|
|
||||||
filter={row.filter}
|
|
||||||
setFilter={filter => {
|
|
||||||
changeColumn({ ...row, filter });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{hasGroupedColumn && (
|
|
||||||
<TableColumn
|
|
||||||
fieldName="groupFilter"
|
|
||||||
header="Group filter"
|
|
||||||
formatter={row => (
|
|
||||||
<DataFilterControl
|
|
||||||
filterType={findDesignerFilterType(row, value)}
|
|
||||||
filter={row.groupFilter}
|
|
||||||
setFilter={groupFilter => {
|
|
||||||
changeColumn({ ...row, groupFilter });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TableColumn
|
|
||||||
fieldName="actions"
|
|
||||||
header=""
|
|
||||||
formatter={row => (
|
|
||||||
<>
|
|
||||||
<InlineButton onClick={() => removeColumn(row)}>Remove</InlineButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableControl>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ToolbarButton from '../widgets/ToolbarButton';
|
|
||||||
|
|
||||||
export default function QueryDesignToolbar({
|
|
||||||
execute,
|
|
||||||
isDatabaseDefined,
|
|
||||||
busy,
|
|
||||||
modelState,
|
|
||||||
dispatchModel,
|
|
||||||
isConnected,
|
|
||||||
kill,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ToolbarButton disabled={!isDatabaseDefined || busy} onClick={execute} icon="icon run">
|
|
||||||
Execute
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close">
|
|
||||||
Kill
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
|
|
||||||
Undo
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!modelState.canRedo} onClick={() => dispatchModel({ type: 'redo' })} icon="icon redo">
|
|
||||||
Redo
|
|
||||||
</ToolbarButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import Designer from './Designer';
|
|
||||||
|
|
||||||
export default function QueryDesigner({ value, conid, database, engine, onChange }) {
|
|
||||||
return <Designer value={value} onChange={onChange} conid={conid} database={database}></Designer>;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export default function cleanupDesignColumns(columns) {
|
|
||||||
return (columns || []).filter(
|
|
||||||
x => x.isOutput || x.isGrouped || x.alias || (x.aggregate && x.aggregate != '---') || x.sortOrder || x.filter
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { dumpSqlSelect, Select, JoinType, Condition, Relation, mergeConditions, Source } from 'dbgate-sqltree';
|
|
||||||
import { EngineDriver } from 'dbgate-types';
|
|
||||||
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
|
|
||||||
import { DesignerComponentCreator } from './DesignerComponentCreator';
|
|
||||||
import { DesignerQueryDumper } from './DesignerQueryDumper';
|
|
||||||
import { getFilterType } from 'dbgate-filterparser';
|
|
||||||
|
|
||||||
export function referenceIsConnecting(
|
|
||||||
reference: DesignerReferenceInfo,
|
|
||||||
tables1: DesignerTableInfo[],
|
|
||||||
tables2: DesignerTableInfo[]
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
(tables1.find(x => x.designerId == reference.sourceId) && tables2.find(x => x.designerId == reference.targetId)) ||
|
|
||||||
(tables1.find(x => x.designerId == reference.targetId) && tables2.find(x => x.designerId == reference.sourceId))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function referenceIsJoin(reference) {
|
|
||||||
return ['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL OUTER JOIN'].includes(reference.joinType);
|
|
||||||
}
|
|
||||||
export function referenceIsExists(reference) {
|
|
||||||
return ['WHERE EXISTS', 'WHERE NOT EXISTS'].includes(reference.joinType);
|
|
||||||
}
|
|
||||||
export function referenceIsCrossJoin(reference) {
|
|
||||||
return !reference.joinType || reference.joinType == 'CROSS JOIN';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findConnectingReference(
|
|
||||||
designer: DesignerInfo,
|
|
||||||
tables1: DesignerTableInfo[],
|
|
||||||
tables2: DesignerTableInfo[],
|
|
||||||
additionalCondition: (ref: DesignerReferenceInfo) => boolean
|
|
||||||
) {
|
|
||||||
for (const ref of designer.references || []) {
|
|
||||||
if (additionalCondition(ref) && referenceIsConnecting(ref, tables1, tables2)) {
|
|
||||||
return ref;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findQuerySource(designer: DesignerInfo, designerId: string): Source {
|
|
||||||
const table = designer.tables.find(x => x.designerId == designerId);
|
|
||||||
if (!table) return null;
|
|
||||||
return {
|
|
||||||
name: table,
|
|
||||||
alias: table.alias,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergeSelectsFromDesigner(select1: Select, select2: Select): Select {
|
|
||||||
return {
|
|
||||||
commandType: 'select',
|
|
||||||
from: {
|
|
||||||
...select1.from,
|
|
||||||
relations: [
|
|
||||||
...select1.from.relations,
|
|
||||||
{
|
|
||||||
joinType: 'CROSS JOIN',
|
|
||||||
name: select2.from.name,
|
|
||||||
alias: select2.from.alias,
|
|
||||||
},
|
|
||||||
...select2.from.relations,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
where: mergeConditions(select1.where, select2.where),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findPrimaryTable(tables: DesignerTableInfo[]) {
|
|
||||||
return _.minBy(tables, x => x.top);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getReferenceConditions(reference: DesignerReferenceInfo, designer: DesignerInfo): Condition[] {
|
|
||||||
const sourceTable = designer.tables.find(x => x.designerId == reference.sourceId);
|
|
||||||
const targetTable = designer.tables.find(x => x.designerId == reference.targetId);
|
|
||||||
|
|
||||||
return reference.columns.map(col => ({
|
|
||||||
conditionType: 'binary',
|
|
||||||
operator: '=',
|
|
||||||
left: {
|
|
||||||
exprType: 'column',
|
|
||||||
columnName: col.source,
|
|
||||||
source: {
|
|
||||||
name: sourceTable,
|
|
||||||
alias: sourceTable.alias,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
right: {
|
|
||||||
exprType: 'column',
|
|
||||||
columnName: col.target,
|
|
||||||
source: {
|
|
||||||
name: targetTable,
|
|
||||||
alias: targetTable.alias,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateDesignedQuery(designer: DesignerInfo, engine: EngineDriver) {
|
|
||||||
const { tables, columns, references } = designer;
|
|
||||||
const primaryTable = findPrimaryTable(designer.tables);
|
|
||||||
if (!primaryTable) return '';
|
|
||||||
const componentCreator = new DesignerComponentCreator(designer);
|
|
||||||
const designerDumper = new DesignerQueryDumper(designer, componentCreator.components);
|
|
||||||
const select = designerDumper.run();
|
|
||||||
|
|
||||||
const dmp = engine.createDumper();
|
|
||||||
dumpSqlSelect(dmp, select);
|
|
||||||
return dmp.s;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isConnectedByReference(
|
|
||||||
designer: DesignerInfo,
|
|
||||||
table1: { designerId: string },
|
|
||||||
table2: { designerId: string },
|
|
||||||
withoutRef: { designerId: string }
|
|
||||||
) {
|
|
||||||
if (!designer.references) return false;
|
|
||||||
const creator = new DesignerComponentCreator({
|
|
||||||
...designer,
|
|
||||||
references: withoutRef
|
|
||||||
? designer.references.filter(x => x.designerId != withoutRef.designerId)
|
|
||||||
: designer.references,
|
|
||||||
});
|
|
||||||
const arrays = creator.components.map(x => x.thisAndSubComponentsTables);
|
|
||||||
const array1 = arrays.find(a => a.find(x => x.designerId == table1.designerId));
|
|
||||||
const array2 = arrays.find(a => a.find(x => x.designerId == table2.designerId));
|
|
||||||
return array1 == array2;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findDesignerFilterType({ designerId, columnName }, designer) {
|
|
||||||
const table = (designer.tables || []).find(x => x.designerId == designerId);
|
|
||||||
if (table) {
|
|
||||||
const column = (table.columns || []).find(x => x.columnName == columnName);
|
|
||||||
if (column) {
|
|
||||||
const { dataType } = column;
|
|
||||||
return getFilterType(dataType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'string';
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { JoinType } from 'dbgate-sqltree';
|
|
||||||
import { TableInfo } from 'dbgate-types';
|
|
||||||
|
|
||||||
export type DesignerTableInfo = TableInfo & {
|
|
||||||
designerId: string;
|
|
||||||
alias?: string;
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DesignerJoinType = JoinType | 'WHERE EXISTS' | 'WHERE NOT EXISTS';
|
|
||||||
|
|
||||||
export type DesignerReferenceInfo = {
|
|
||||||
designerId: string;
|
|
||||||
joinType: DesignerJoinType;
|
|
||||||
sourceId: string;
|
|
||||||
targetId: string;
|
|
||||||
columns: {
|
|
||||||
source: string;
|
|
||||||
target: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DesignerColumnInfo = {
|
|
||||||
designerId: string;
|
|
||||||
columnName: string;
|
|
||||||
alias?: string;
|
|
||||||
isGrouped?: boolean;
|
|
||||||
aggregate?: string;
|
|
||||||
isOutput?: boolean;
|
|
||||||
sortOrder?: number;
|
|
||||||
filter?: string;
|
|
||||||
groupFilter?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DesignerInfo = {
|
|
||||||
tables: DesignerTableInfo[];
|
|
||||||
columns: DesignerColumnInfo[];
|
|
||||||
references: DesignerReferenceInfo[];
|
|
||||||
};
|
|
||||||
|
|
||||||
// export type DesignerComponent = {
|
|
||||||
// tables: DesignerTableInfo[];
|
|
||||||
// };
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeSet,
|
|
||||||
changeSetContainsChanges,
|
|
||||||
changeSetInsertNewRow,
|
|
||||||
createChangeSet,
|
|
||||||
deleteChangeSetRows,
|
|
||||||
findExistingChangeSetItem,
|
|
||||||
getChangeSetInsertedRows,
|
|
||||||
TableFormViewDisplay,
|
|
||||||
revertChangeSetRowChanges,
|
|
||||||
setChangeSetValue,
|
|
||||||
ChangeSetRowDefinition,
|
|
||||||
} from 'dbgate-datalib';
|
|
||||||
import Former from './Former';
|
|
||||||
|
|
||||||
export default class ChangeSetFormer extends Former {
|
|
||||||
public changeSet: ChangeSet;
|
|
||||||
public setChangeSet: Function;
|
|
||||||
private batchChangeSet: ChangeSet;
|
|
||||||
public rowDefinition: ChangeSetRowDefinition;
|
|
||||||
public rowStatus;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public sourceRow: any,
|
|
||||||
public changeSetState,
|
|
||||||
public dispatchChangeSet,
|
|
||||||
public display: TableFormViewDisplay
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.changeSet = changeSetState && changeSetState.value;
|
|
||||||
this.setChangeSet = value => dispatchChangeSet({ type: 'set', value });
|
|
||||||
this.batchChangeSet = null;
|
|
||||||
this.rowDefinition = display.getChangeSetRow(sourceRow);
|
|
||||||
const [matchedField, matchedChangeSetItem] = findExistingChangeSetItem(this.changeSet, this.rowDefinition);
|
|
||||||
this.rowData = matchedChangeSetItem ? { ...sourceRow, ...matchedChangeSetItem.fields } : sourceRow;
|
|
||||||
let status = 'regular';
|
|
||||||
if (matchedChangeSetItem && matchedField == 'updates') status = 'updated';
|
|
||||||
if (matchedField == 'deletes') status = 'deleted';
|
|
||||||
this.rowStatus = {
|
|
||||||
status,
|
|
||||||
modifiedFields:
|
|
||||||
matchedChangeSetItem && matchedChangeSetItem.fields ? new Set(Object.keys(matchedChangeSetItem.fields)) : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
applyModification(changeSetReducer) {
|
|
||||||
if (this.batchChangeSet) {
|
|
||||||
this.batchChangeSet = changeSetReducer(this.batchChangeSet);
|
|
||||||
} else {
|
|
||||||
this.setChangeSet(changeSetReducer(this.changeSet));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCellValue(uniqueName: string, value: any) {
|
|
||||||
const row = this.sourceRow;
|
|
||||||
const definition = this.display.getChangeSetField(row, uniqueName);
|
|
||||||
this.applyModification(chs => setChangeSetValue(chs, definition, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteRow(index: number) {
|
|
||||||
this.applyModification(chs => deleteChangeSetRows(chs, this.rowDefinition));
|
|
||||||
}
|
|
||||||
|
|
||||||
beginUpdate() {
|
|
||||||
this.batchChangeSet = this.changeSet;
|
|
||||||
}
|
|
||||||
endUpdate() {
|
|
||||||
this.setChangeSet(this.batchChangeSet);
|
|
||||||
this.batchChangeSet = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
revertRowChanges() {
|
|
||||||
this.applyModification(chs => revertChangeSetRowChanges(chs, this.rowDefinition));
|
|
||||||
}
|
|
||||||
revertAllChanges() {
|
|
||||||
this.applyModification(chs => createChangeSet());
|
|
||||||
}
|
|
||||||
undo() {
|
|
||||||
this.dispatchChangeSet({ type: 'undo' });
|
|
||||||
}
|
|
||||||
redo() {
|
|
||||||
this.dispatchChangeSet({ type: 'redo' });
|
|
||||||
}
|
|
||||||
get canUndo() {
|
|
||||||
return this.changeSetState.canUndo;
|
|
||||||
}
|
|
||||||
get canRedo() {
|
|
||||||
return this.changeSetState.canRedo;
|
|
||||||
}
|
|
||||||
get containsChanges() {
|
|
||||||
return changeSetContainsChanges(this.changeSet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,583 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import ColumnLabel from '../datagrid/ColumnLabel';
|
|
||||||
import { findForeignKeyForColumn } from 'dbgate-tools';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import useDimensions from '../utility/useDimensions';
|
|
||||||
import FormViewToolbar from './FormViewToolbar';
|
|
||||||
import { useShowMenu } from '../modals/showMenu';
|
|
||||||
import FormViewContextMenu from './FormViewContextMenu';
|
|
||||||
import keycodes from '../utility/keycodes';
|
|
||||||
import { CellFormattedValue, ShowFormButton } from '../datagrid/DataGridRow';
|
|
||||||
import { cellFromEvent } from '../datagrid/selection';
|
|
||||||
import InplaceEditor from '../datagrid/InplaceEditor';
|
|
||||||
import { copyTextToClipboard } from '../utility/clipboard';
|
|
||||||
import { ExpandIcon, FontIcon } from '../icons';
|
|
||||||
import openReferenceForm from './openReferenceForm';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
import LoadingInfo from '../widgets/LoadingInfo';
|
|
||||||
|
|
||||||
const Table = styled.table`
|
|
||||||
border-collapse: collapse;
|
|
||||||
outline: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const OuterWrapper = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
overflow-x: scroll;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TableRow = styled.tr`
|
|
||||||
background-color: ${props => props.theme.gridbody_background};
|
|
||||||
&:nth-child(6n + 3) {
|
|
||||||
background-color: ${props => props.theme.gridbody_background_alt2};
|
|
||||||
}
|
|
||||||
&:nth-child(6n + 6) {
|
|
||||||
background-color: ${props => props.theme.gridbody_background_alt3};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TableHeaderCell = styled.td`
|
|
||||||
border: 1px solid ${props => props.theme.border};
|
|
||||||
text-align: left;
|
|
||||||
padding: 2px;
|
|
||||||
background-color: ${props => props.theme.gridheader_background};
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
props.isSelected &&
|
|
||||||
`
|
|
||||||
background: initial;
|
|
||||||
background-color: ${props.theme.gridbody_selection[4]};
|
|
||||||
color: ${props.theme.gridbody_invfont1};`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TableBodyCell = styled.td`
|
|
||||||
font-weight: normal;
|
|
||||||
border: 1px solid ${props => props.theme.border};
|
|
||||||
// border-collapse: collapse;
|
|
||||||
padding: 2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
max-width: 500px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
props.isSelected &&
|
|
||||||
`
|
|
||||||
background: initial;
|
|
||||||
background-color: ${props.theme.gridbody_selection[4]};
|
|
||||||
color: ${props.theme.gridbody_invfont1};`}
|
|
||||||
|
|
||||||
${props =>
|
|
||||||
!props.isSelected &&
|
|
||||||
props.isModifiedCell &&
|
|
||||||
`
|
|
||||||
background-color: ${props.theme.gridbody_background_orange[1]};`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FocusField = styled.input`
|
|
||||||
// visibility: hidden
|
|
||||||
position: absolute;
|
|
||||||
left: -1000px;
|
|
||||||
top: -1000px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const RowCountLabel = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
background-color: ${props => props.theme.gridbody_background_yellow[1]};
|
|
||||||
right: 40px;
|
|
||||||
bottom: 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HintSpan = styled.span`
|
|
||||||
color: gray;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ColumnLabelMargin = styled(ColumnLabel)`
|
|
||||||
margin-right: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function isDataCell(cell) {
|
|
||||||
return cell[1] % 2 == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FormView(props) {
|
|
||||||
const {
|
|
||||||
toolbarPortalRef,
|
|
||||||
tabVisible,
|
|
||||||
config,
|
|
||||||
setConfig,
|
|
||||||
onNavigate,
|
|
||||||
former,
|
|
||||||
onSave,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
onReload,
|
|
||||||
onReconnect,
|
|
||||||
allRowCount,
|
|
||||||
rowCountBefore,
|
|
||||||
onSelectionChanged,
|
|
||||||
isLoading,
|
|
||||||
} = props;
|
|
||||||
/** @type {import('dbgate-datalib').FormViewDisplay} */
|
|
||||||
const formDisplay = props.formDisplay;
|
|
||||||
const theme = useTheme();
|
|
||||||
const [headerRowRef, { height: rowHeight }] = useDimensions();
|
|
||||||
const [wrapperRef, { height: wrapperHeight }] = useDimensions();
|
|
||||||
const showMenu = useShowMenu();
|
|
||||||
const focusFieldRef = React.useRef(null);
|
|
||||||
const [currentCell, setCurrentCell] = React.useState([0, 0]);
|
|
||||||
const cellRefs = React.useRef({});
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
const rowCount = Math.floor((wrapperHeight - 20) / rowHeight);
|
|
||||||
const columnChunks = _.chunk(formDisplay.columns, rowCount);
|
|
||||||
|
|
||||||
const { rowData, rowStatus } = former;
|
|
||||||
|
|
||||||
const handleSwitchToTable = () => {
|
|
||||||
setConfig(cfg => ({
|
|
||||||
...cfg,
|
|
||||||
isFormView: false,
|
|
||||||
formViewKey: null,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterThisValue = isDataCell(currentCell)
|
|
||||||
? () => formDisplay.filterCellValue(getCellColumn(currentCell), rowData)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const handleContextMenu = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
showMenu(
|
|
||||||
event.pageX,
|
|
||||||
event.pageY,
|
|
||||||
<FormViewContextMenu
|
|
||||||
switchToTable={handleSwitchToTable}
|
|
||||||
onNavigate={onNavigate}
|
|
||||||
addToFilter={() => formDisplay.addFilterColumn(getCellColumn(currentCell))}
|
|
||||||
filterThisValue={handleFilterThisValue}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCellRef = (row, col, element) => {
|
|
||||||
cellRefs.current[`${row},${col}`] = element;
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (tabVisible) {
|
|
||||||
if (focusFieldRef.current) focusFieldRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [tabVisible, focusFieldRef.current]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!onSelectionChanged || !rowData) return;
|
|
||||||
const col = getCellColumn(currentCell);
|
|
||||||
if (!col) return;
|
|
||||||
onSelectionChanged(rowData[col.uniqueName]);
|
|
||||||
}, [onSelectionChanged, currentCell, rowData]);
|
|
||||||
|
|
||||||
const checkMoveCursorBounds = (row, col) => {
|
|
||||||
if (row < 0) row = 0;
|
|
||||||
if (col < 0) col = 0;
|
|
||||||
if (col >= columnChunks.length * 2) col = columnChunks.length * 2 - 1;
|
|
||||||
const chunk = columnChunks[Math.floor(col / 2)];
|
|
||||||
if (chunk && row >= chunk.length) row = chunk.length - 1;
|
|
||||||
return [row, col];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCursorMove = event => {
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
switch (event.keyCode) {
|
|
||||||
case keycodes.leftArrow:
|
|
||||||
return checkMoveCursorBounds(currentCell[0], 0);
|
|
||||||
case keycodes.rightArrow:
|
|
||||||
return checkMoveCursorBounds(currentCell[0], columnChunks.length * 2 - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (event.keyCode) {
|
|
||||||
case keycodes.leftArrow:
|
|
||||||
return checkMoveCursorBounds(currentCell[0], currentCell[1] - 1);
|
|
||||||
case keycodes.rightArrow:
|
|
||||||
return checkMoveCursorBounds(currentCell[0], currentCell[1] + 1);
|
|
||||||
case keycodes.upArrow:
|
|
||||||
return checkMoveCursorBounds(currentCell[0] - 1, currentCell[1]);
|
|
||||||
case keycodes.downArrow:
|
|
||||||
return checkMoveCursorBounds(currentCell[0] + 1, currentCell[1]);
|
|
||||||
case keycodes.pageUp:
|
|
||||||
return checkMoveCursorBounds(0, currentCell[1]);
|
|
||||||
case keycodes.pageDown:
|
|
||||||
return checkMoveCursorBounds(rowCount - 1, currentCell[1]);
|
|
||||||
case keycodes.home:
|
|
||||||
return checkMoveCursorBounds(0, 0);
|
|
||||||
case keycodes.end:
|
|
||||||
return checkMoveCursorBounds(rowCount - 1, columnChunks.length * 2 - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyNavigation = event => {
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
switch (event.keyCode) {
|
|
||||||
case keycodes.upArrow:
|
|
||||||
return 'previous';
|
|
||||||
case keycodes.downArrow:
|
|
||||||
return 'next';
|
|
||||||
case keycodes.home:
|
|
||||||
return 'begin';
|
|
||||||
case keycodes.end:
|
|
||||||
return 'end';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleSave() {
|
|
||||||
if (inplaceEditorState.cell) {
|
|
||||||
// @ts-ignore
|
|
||||||
dispatchInsplaceEditor({ type: 'shouldSave' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (onSave) onSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCellColumn(cell) {
|
|
||||||
const chunk = columnChunks[Math.floor(cell[1] / 2)];
|
|
||||||
if (!chunk) return;
|
|
||||||
const column = chunk[cell[0]];
|
|
||||||
return column;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCellValue(cell, value) {
|
|
||||||
const column = getCellColumn(cell);
|
|
||||||
if (!column) return;
|
|
||||||
former.setCellValue(column.uniqueName, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNull() {
|
|
||||||
if (isDataCell(currentCell)) {
|
|
||||||
setCellValue(currentCell, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollIntoView = cell => {
|
|
||||||
const element = cellRefs.current[`${cell[0]},${cell[1]}`];
|
|
||||||
if (element) element.scrollIntoView();
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
scrollIntoView(currentCell);
|
|
||||||
}, [rowData]);
|
|
||||||
|
|
||||||
const moveCurrentCell = (row, col) => {
|
|
||||||
const moved = checkMoveCursorBounds(row, col);
|
|
||||||
setCurrentCell(moved);
|
|
||||||
scrollIntoView(moved);
|
|
||||||
};
|
|
||||||
|
|
||||||
function copyToClipboard() {
|
|
||||||
const column = getCellColumn(currentCell);
|
|
||||||
if (!column) return;
|
|
||||||
const text = currentCell[1] % 2 == 1 ? rowData[column.uniqueName] : column.columnName;
|
|
||||||
copyTextToClipboard(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = event => {
|
|
||||||
const navigation = handleKeyNavigation(event);
|
|
||||||
if (navigation) {
|
|
||||||
event.preventDefault();
|
|
||||||
onNavigate(navigation);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const moved = handleCursorMove(event);
|
|
||||||
if (moved) {
|
|
||||||
setCurrentCell(moved);
|
|
||||||
scrollIntoView(moved);
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.keyCode == keycodes.s && event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
handleSave();
|
|
||||||
// this.saveAndFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.keyCode == keycodes.n0 && event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
setNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.keyCode == keycodes.r && event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
former.revertRowChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (event.keyCode == keycodes.f && event.ctrlKey) {
|
|
||||||
// event.preventDefault();
|
|
||||||
// filterSelectedValue();
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (event.keyCode == keycodes.z && event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
former.undo();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.keyCode == keycodes.y && event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
former.redo();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.keyCode == keycodes.c && event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
copyToClipboard();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.keyCode == keycodes.f && event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (handleFilterThisValue) handleFilterThisValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.keyCode == keycodes.f5) {
|
|
||||||
event.preventDefault();
|
|
||||||
onReload();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.keyCode == keycodes.f4) {
|
|
||||||
event.preventDefault();
|
|
||||||
handleSwitchToTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
rowData &&
|
|
||||||
!event.ctrlKey &&
|
|
||||||
!event.altKey &&
|
|
||||||
((event.keyCode >= keycodes.a && event.keyCode <= keycodes.z) ||
|
|
||||||
(event.keyCode >= keycodes.n0 && event.keyCode <= keycodes.n9) ||
|
|
||||||
event.keyCode == keycodes.dash)
|
|
||||||
) {
|
|
||||||
// @ts-ignore
|
|
||||||
dispatchInsplaceEditor({ type: 'show', text: event.nativeEvent.key, cell: currentCell });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (rowData && event.keyCode == keycodes.f2) {
|
|
||||||
// @ts-ignore
|
|
||||||
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTableMouseDown = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (focusFieldRef.current) focusFieldRef.current.focus();
|
|
||||||
|
|
||||||
if (event.target.closest('.buttonLike')) return;
|
|
||||||
if (event.target.closest('.resizeHandleControl')) return;
|
|
||||||
if (event.target.closest('input')) return;
|
|
||||||
|
|
||||||
// event.target.closest('table').focus();
|
|
||||||
event.preventDefault();
|
|
||||||
if (focusFieldRef.current) focusFieldRef.current.focus();
|
|
||||||
const cell = cellFromEvent(event);
|
|
||||||
|
|
||||||
if (isDataCell(cell) && !_.isEqual(cell, inplaceEditorState.cell) && _.isEqual(cell, currentCell)) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (rowData) {
|
|
||||||
dispatchInsplaceEditor({ type: 'show', cell, selectAll: true });
|
|
||||||
}
|
|
||||||
} else if (!_.isEqual(cell, inplaceEditorState.cell)) {
|
|
||||||
// @ts-ignore
|
|
||||||
dispatchInsplaceEditor({ type: 'close' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
setCurrentCell(cell);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCellWidth = (row, col) => {
|
|
||||||
const element = cellRefs.current[`${row},${col}`];
|
|
||||||
if (element) return element.getBoundingClientRect().width;
|
|
||||||
return 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
const rowCountInfo = React.useMemo(() => {
|
|
||||||
if (rowData == null) return 'No data';
|
|
||||||
if (allRowCount == null || rowCountBefore == null) return 'Loading row count...';
|
|
||||||
return `Row: ${(rowCountBefore + 1).toLocaleString()} / ${allRowCount.toLocaleString()}`;
|
|
||||||
}, [rowCountBefore, allRowCount]);
|
|
||||||
|
|
||||||
const [inplaceEditorState, dispatchInsplaceEditor] = React.useReducer((state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'show': {
|
|
||||||
const column = getCellColumn(action.cell);
|
|
||||||
if (!column) return state;
|
|
||||||
if (column.uniquePath.length > 1) return state;
|
|
||||||
|
|
||||||
// if (!grider.editable) return {};
|
|
||||||
return {
|
|
||||||
cell: action.cell,
|
|
||||||
text: action.text,
|
|
||||||
selectAll: action.selectAll,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'close': {
|
|
||||||
const [row, col] = currentCell || [];
|
|
||||||
if (focusFieldRef.current) focusFieldRef.current.focus();
|
|
||||||
// @ts-ignore
|
|
||||||
if (action.mode == 'enter' && row) setTimeout(() => moveCurrentCell(row + 1, col), 0);
|
|
||||||
// if (action.mode == 'save') setTimeout(handleSave, 0);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
case 'shouldSave': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
shouldSave: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const toolbar =
|
|
||||||
toolbarPortalRef &&
|
|
||||||
toolbarPortalRef.current &&
|
|
||||||
tabVisible &&
|
|
||||||
ReactDOM.createPortal(
|
|
||||||
<FormViewToolbar
|
|
||||||
switchToTable={handleSwitchToTable}
|
|
||||||
onNavigate={onNavigate}
|
|
||||||
reload={onReload}
|
|
||||||
reconnect={onReconnect}
|
|
||||||
save={handleSave}
|
|
||||||
former={former}
|
|
||||||
/>,
|
|
||||||
toolbarPortalRef.current
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LoadingInfo wrapper message="Loading data" />
|
|
||||||
{toolbar}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!formDisplay || !formDisplay.isLoadedCorrectly) return toolbar;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OuterWrapper>
|
|
||||||
<Wrapper ref={wrapperRef} onContextMenu={handleContextMenu}>
|
|
||||||
{columnChunks.map((chunk, chunkIndex) => (
|
|
||||||
<Table key={chunkIndex} onMouseDown={handleTableMouseDown}>
|
|
||||||
{chunk.map((col, rowIndex) => (
|
|
||||||
<TableRow key={col.uniqueName} theme={theme} ref={headerRowRef} style={{ height: `${rowHeight}px` }}>
|
|
||||||
<TableHeaderCell
|
|
||||||
theme={theme}
|
|
||||||
data-row={rowIndex}
|
|
||||||
data-col={chunkIndex * 2}
|
|
||||||
// @ts-ignore
|
|
||||||
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2}
|
|
||||||
ref={element => setCellRef(rowIndex, chunkIndex * 2, element)}
|
|
||||||
>
|
|
||||||
<ColumnLabelMargin
|
|
||||||
{...col}
|
|
||||||
headerText={col.columnName}
|
|
||||||
style={{ marginLeft: (col.uniquePath.length - 1) * 20 }}
|
|
||||||
extInfo={col.foreignKey ? ` -> ${col.foreignKey.refTableName}` : null}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{col.foreignKey && (
|
|
||||||
<ShowFormButton
|
|
||||||
theme={theme}
|
|
||||||
className="buttonLike"
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
formDisplay.toggleExpandedColumn(col.uniqueName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExpandIcon isExpanded={formDisplay.isExpandedColumn(col.uniqueName)} />
|
|
||||||
</ShowFormButton>
|
|
||||||
)}
|
|
||||||
</TableHeaderCell>
|
|
||||||
<TableBodyCell
|
|
||||||
theme={theme}
|
|
||||||
data-row={rowIndex}
|
|
||||||
data-col={chunkIndex * 2 + 1}
|
|
||||||
// @ts-ignore
|
|
||||||
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1}
|
|
||||||
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
|
||||||
ref={element => setCellRef(rowIndex, chunkIndex * 2 + 1, element)}
|
|
||||||
>
|
|
||||||
{inplaceEditorState.cell &&
|
|
||||||
rowIndex == inplaceEditorState.cell[0] &&
|
|
||||||
chunkIndex * 2 + 1 == inplaceEditorState.cell[1] ? (
|
|
||||||
<InplaceEditor
|
|
||||||
widthPx={getCellWidth(rowIndex, chunkIndex * 2 + 1)}
|
|
||||||
inplaceEditorState={inplaceEditorState}
|
|
||||||
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
|
||||||
cellValue={rowData[col.uniqueName]}
|
|
||||||
onSetValue={value => {
|
|
||||||
former.setCellValue(col.uniqueName, value);
|
|
||||||
}}
|
|
||||||
// grider={grider}
|
|
||||||
// rowIndex={rowIndex}
|
|
||||||
// uniqueName={col.uniqueName}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{rowData && (
|
|
||||||
<CellFormattedValue value={rowData[col.uniqueName]} dataType={col.dataType} theme={theme} />
|
|
||||||
)}
|
|
||||||
{!!col.hintColumnName &&
|
|
||||||
rowData &&
|
|
||||||
!(rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) && (
|
|
||||||
<HintSpan>{rowData[col.hintColumnName]}</HintSpan>
|
|
||||||
)}
|
|
||||||
{col.foreignKey && rowData && rowData[col.uniqueName] && (
|
|
||||||
<ShowFormButton
|
|
||||||
theme={theme}
|
|
||||||
className="buttonLike"
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
openReferenceForm(rowData, col, openNewTab, conid, database);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontIcon icon="icon form" />
|
|
||||||
</ShowFormButton>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TableBodyCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</Table>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<FocusField type="text" ref={focusFieldRef} onKeyDown={handleKeyDown} />
|
|
||||||
|
|
||||||
{toolbar}
|
|
||||||
</Wrapper>
|
|
||||||
{rowCountInfo && <RowCountLabel theme={theme}>{rowCountInfo}</RowCountLabel>}
|
|
||||||
</OuterWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
|
||||||
|
|
||||||
export default function FormViewContextMenu({ switchToTable, onNavigate, addToFilter, filterThisValue }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DropDownMenuItem onClick={switchToTable} keyText="F4">
|
|
||||||
Table view
|
|
||||||
</DropDownMenuItem>
|
|
||||||
{addToFilter && <DropDownMenuItem onClick={addToFilter}>Add to filter</DropDownMenuItem>}
|
|
||||||
{filterThisValue && (
|
|
||||||
<DropDownMenuItem onClick={filterThisValue} keyText="Ctrl+F">
|
|
||||||
Filter this value
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
<DropDownMenuItem onClick={() => onNavigate('begin')} keyText="Ctrl+Home">
|
|
||||||
Navigate to begin
|
|
||||||
</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => onNavigate('previous')} keyText="Ctrl+Up">
|
|
||||||
Navigate to previous
|
|
||||||
</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => onNavigate('next')} keyText="Ctrl+Down">
|
|
||||||
Navigate to next
|
|
||||||
</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => onNavigate('end')} keyText="Ctrl+End">
|
|
||||||
Navigate to end
|
|
||||||
</DropDownMenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import ColumnLabel from '../datagrid/ColumnLabel';
|
|
||||||
import { TextField } from '../utility/inputs';
|
|
||||||
import { getFilterType } from 'dbgate-filterparser';
|
|
||||||
import DataFilterControl from '../datagrid/DataFilterControl';
|
|
||||||
import InlineButton from '../widgets/InlineButton';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import keycodes from '../utility/keycodes';
|
|
||||||
|
|
||||||
const ColumnWrapper = styled.div`
|
|
||||||
margin: 5px;
|
|
||||||
`;
|
|
||||||
const ColumnNameWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TextFieldWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
const StyledTextField = styled(TextField)`
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function PrimaryKeyFilterEditor({ column, baseTable, formDisplay }) {
|
|
||||||
const value = formDisplay.getKeyValue(column.columnName);
|
|
||||||
const editorRef = React.useRef(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (editorRef.current) {
|
|
||||||
editorRef.current.value = value;
|
|
||||||
}
|
|
||||||
}, [value, editorRef.current]);
|
|
||||||
|
|
||||||
const applyFilter = () => {
|
|
||||||
formDisplay.requestKeyValue(column.columnName, editorRef.current.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelFilter = () => {
|
|
||||||
formDisplay.cancelRequestKey();
|
|
||||||
formDisplay.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = ev => {
|
|
||||||
if (ev.keyCode == keycodes.enter) {
|
|
||||||
applyFilter();
|
|
||||||
}
|
|
||||||
if (ev.keyCode == keycodes.escape) {
|
|
||||||
cancelFilter();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ColumnWrapper>
|
|
||||||
<ColumnNameWrapper>
|
|
||||||
<div>
|
|
||||||
<FontIcon icon="img primary-key" />
|
|
||||||
<ColumnLabel {...baseTable.columns.find(x => x.columnName == column.columnName)} />
|
|
||||||
</div>
|
|
||||||
{formDisplay.config.formViewKeyRequested && (
|
|
||||||
<InlineButton square onClick={cancelFilter}>
|
|
||||||
<FontIcon icon="icon delete" />
|
|
||||||
</InlineButton>
|
|
||||||
)}
|
|
||||||
</ColumnNameWrapper>
|
|
||||||
<TextFieldWrapper>
|
|
||||||
<StyledTextField editorRef={editorRef} onBlur={applyFilter} onKeyDown={handleKeyDown} />
|
|
||||||
</TextFieldWrapper>
|
|
||||||
</ColumnWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FormViewFilters(props) {
|
|
||||||
const { formDisplay } = props;
|
|
||||||
if (!formDisplay || !formDisplay.baseTable || !formDisplay.baseTable.primaryKey) return null;
|
|
||||||
const { baseTable } = formDisplay;
|
|
||||||
const { formFilterColumns, filters } = formDisplay.config || {};
|
|
||||||
|
|
||||||
const allFilterNames = _.union(_.keys(filters || {}), formFilterColumns || []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
|
|
||||||
{baseTable.primaryKey.columns.map(col => (
|
|
||||||
<PrimaryKeyFilterEditor key={col.columnName} baseTable={baseTable} column={col} formDisplay={formDisplay} />
|
|
||||||
))}
|
|
||||||
{allFilterNames.map(uniqueName => {
|
|
||||||
const column = formDisplay.columns.find(x => x.uniqueName == uniqueName)
|
|
||||||
// const column = baseTable.columns.find(x => x.columnName == columnName);
|
|
||||||
if (!column) return null;
|
|
||||||
return (
|
|
||||||
<ColumnWrapper key={uniqueName}>
|
|
||||||
<ColumnNameWrapper>
|
|
||||||
<ColumnLabel {...column} />
|
|
||||||
<InlineButton
|
|
||||||
square
|
|
||||||
onClick={() => {
|
|
||||||
formDisplay.removeFilter(column.uniqueName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontIcon icon="icon delete" />
|
|
||||||
</InlineButton>
|
|
||||||
</ColumnNameWrapper>
|
|
||||||
<DataFilterControl
|
|
||||||
filterType={getFilterType(column.dataType)}
|
|
||||||
filter={filters[column.uniqueName]}
|
|
||||||
setFilter={value => formDisplay.setFilter(column.uniqueName, value)}
|
|
||||||
/>
|
|
||||||
</ColumnWrapper>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ManagerInnerContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ToolbarButton from '../widgets/ToolbarButton';
|
|
||||||
|
|
||||||
export default function FormViewToolbar({ switchToTable, onNavigate, reload, reconnect, former, save }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ToolbarButton onClick={switchToTable} icon="icon table">
|
|
||||||
Table view
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton onClick={() => onNavigate('begin')} icon="icon arrow-begin">
|
|
||||||
First
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton onClick={() => onNavigate('previous')} icon="icon arrow-left">
|
|
||||||
Previous
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton onClick={() => onNavigate('next')} icon="icon arrow-right">
|
|
||||||
Next
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton onClick={() => onNavigate('end')} icon="icon arrow-end">
|
|
||||||
Last
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton onClick={reload} icon="icon reload">
|
|
||||||
Refresh
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton onClick={reconnect} icon="icon connection">
|
|
||||||
Reconnect
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!former.canUndo} onClick={() => former.undo()} icon="icon undo">
|
|
||||||
Undo
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!former.canRedo} onClick={() => former.redo()} icon="icon redo">
|
|
||||||
Redo
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!former.allowSave} onClick={save} icon="icon save">
|
|
||||||
Save
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton disabled={!former.containsChanges} onClick={() => former.revertAllChanges()} icon="icon close">
|
|
||||||
Revert
|
|
||||||
</ToolbarButton>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// export interface GriderRowStatus {
|
|
||||||
// status: 'regular' | 'updated' | 'deleted' | 'inserted';
|
|
||||||
// modifiedFields?: Set<string>;
|
|
||||||
// insertedFields?: Set<string>;
|
|
||||||
// deletedFields?: Set<string>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export default abstract class Former {
|
|
||||||
public rowData: any;
|
|
||||||
|
|
||||||
// getRowStatus(index): GriderRowStatus {
|
|
||||||
// const res: GriderRowStatus = {
|
|
||||||
// status: 'regular',
|
|
||||||
// };
|
|
||||||
// return res;
|
|
||||||
// }
|
|
||||||
beginUpdate() {}
|
|
||||||
endUpdate() {}
|
|
||||||
setCellValue(uniqueName: string, value: any) {}
|
|
||||||
revertRowChanges() {}
|
|
||||||
revertAllChanges() {}
|
|
||||||
undo() {}
|
|
||||||
redo() {}
|
|
||||||
get editable() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get canInsert() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get allowSave() {
|
|
||||||
return this.containsChanges;
|
|
||||||
}
|
|
||||||
get canUndo() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get canRedo() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get containsChanges() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get disableLoadNextPage() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
get errors() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
updateRow(changeObject) {
|
|
||||||
for (const key of Object.keys(changeObject)) {
|
|
||||||
this.setCellValue(key, changeObject[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
import { changeSetToSql, createChangeSet, TableFormViewDisplay } from 'dbgate-datalib';
|
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
|
||||||
import React from 'react';
|
|
||||||
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
|
||||||
import useExtensions from '../utility/useExtensions';
|
|
||||||
import FormView from './FormView';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import ChangeSetFormer from './ChangeSetFormer';
|
|
||||||
import ConfirmSqlModal from '../modals/ConfirmSqlModal';
|
|
||||||
import ErrorMessageModal from '../modals/ErrorMessageModal';
|
|
||||||
import { scriptToSql } from 'dbgate-sqltree';
|
|
||||||
import useModalState from '../modals/useModalState';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import stableStringify from 'json-stable-stringify';
|
|
||||||
|
|
||||||
async function loadRow(props, sql) {
|
|
||||||
const { conid, database } = props;
|
|
||||||
|
|
||||||
if (!sql) return null;
|
|
||||||
|
|
||||||
const response = await axios.request({
|
|
||||||
url: 'database-connections/query-data',
|
|
||||||
method: 'post',
|
|
||||||
params: {
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
},
|
|
||||||
data: { sql },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.errorMessage) return response.data;
|
|
||||||
return response.data.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SqlFormView(props) {
|
|
||||||
// console.log('SqlFormView', props);
|
|
||||||
const {
|
|
||||||
formDisplay,
|
|
||||||
changeSetState,
|
|
||||||
dispatchChangeSet,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
onReferenceSourceChanged,
|
|
||||||
refReloadToken,
|
|
||||||
} = props;
|
|
||||||
// const [rowData, setRowData] = React.useState(null);
|
|
||||||
// const [reloadToken, setReloadToken] = React.useState(0);
|
|
||||||
// const [rowCountInfo, setRowCountInfo] = React.useState(null);
|
|
||||||
// const [isLoading, setIsLoading] = React.useState(false);
|
|
||||||
// const loadedFiltersRef = React.useRef('');
|
|
||||||
|
|
||||||
const confirmSqlModalState = useModalState();
|
|
||||||
const [confirmSql, setConfirmSql] = React.useState('');
|
|
||||||
const showModal = useShowModal();
|
|
||||||
|
|
||||||
const changeSet = changeSetState && changeSetState.value;
|
|
||||||
const changeSetRef = React.useRef(changeSet);
|
|
||||||
changeSetRef.current = changeSet;
|
|
||||||
|
|
||||||
const [loadProps, setLoadProps] = React.useState({
|
|
||||||
isLoadingData: false,
|
|
||||||
isLoadedData: false,
|
|
||||||
rowData: null,
|
|
||||||
isLoadingCount: false,
|
|
||||||
isLoadedCount: false,
|
|
||||||
loadedTime: new Date().getTime(),
|
|
||||||
allRowCount: null,
|
|
||||||
rowCountBefore: null,
|
|
||||||
errorMessage: null,
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
isLoadingData,
|
|
||||||
rowData,
|
|
||||||
isLoadedData,
|
|
||||||
isLoadingCount,
|
|
||||||
isLoadedCount,
|
|
||||||
loadedTime,
|
|
||||||
allRowCount,
|
|
||||||
rowCountBefore,
|
|
||||||
errorMessage,
|
|
||||||
} = loadProps;
|
|
||||||
|
|
||||||
const handleLoadCurrentRow = async () => {
|
|
||||||
if (isLoadingData) return;
|
|
||||||
let newLoadedRow = false;
|
|
||||||
if (formDisplay.config.formViewKeyRequested || formDisplay.config.formViewKey) {
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
isLoadingData: true,
|
|
||||||
}));
|
|
||||||
const row = await loadRow(props, formDisplay.getCurrentRowQuery());
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
isLoadingData: false,
|
|
||||||
isLoadedData: true,
|
|
||||||
rowData: row,
|
|
||||||
loadedTime: new Date().getTime(),
|
|
||||||
}));
|
|
||||||
newLoadedRow = row;
|
|
||||||
}
|
|
||||||
if (formDisplay.config.formViewKeyRequested && newLoadedRow) {
|
|
||||||
formDisplay.cancelRequestKey(newLoadedRow);
|
|
||||||
}
|
|
||||||
if (!newLoadedRow && !formDisplay.config.formViewKeyRequested) {
|
|
||||||
await handleNavigate('first');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadRowCount = async () => {
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
isLoadingCount: true,
|
|
||||||
}));
|
|
||||||
const countRow = await loadRow(props, formDisplay.getCountQuery());
|
|
||||||
const countBeforeRow = await loadRow(props, formDisplay.getBeforeCountQuery());
|
|
||||||
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
isLoadedCount: true,
|
|
||||||
isLoadingCount: false,
|
|
||||||
allRowCount: countRow ? parseInt(countRow.count) : null,
|
|
||||||
rowCountBefore: countBeforeRow ? parseInt(countBeforeRow.count) : null,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNavigate = async command => {
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
isLoadingData: true,
|
|
||||||
}));
|
|
||||||
const row = await loadRow(props, formDisplay.navigateRowQuery(command));
|
|
||||||
if (row) {
|
|
||||||
formDisplay.navigate(row);
|
|
||||||
}
|
|
||||||
setLoadProps(oldLoadProps => ({
|
|
||||||
...oldLoadProps,
|
|
||||||
isLoadingData: false,
|
|
||||||
isLoadedData: true,
|
|
||||||
isLoadedCount: false,
|
|
||||||
allRowCount: null,
|
|
||||||
rowCountBefore: null,
|
|
||||||
rowData: row,
|
|
||||||
loadedTime: new Date().getTime(),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (onReferenceSourceChanged && rowData) onReferenceSourceChanged([rowData], loadedTime);
|
|
||||||
}, [onReferenceSourceChanged, rowData, refReloadToken]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!formDisplay.isLoadedCorrectly) return;
|
|
||||||
if (!isLoadedData && !isLoadingData) handleLoadCurrentRow();
|
|
||||||
if (isLoadedData && !isLoadingCount && !isLoadedCount) handleLoadRowCount();
|
|
||||||
});
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// loadedFiltersRef.current = formDisplay ? stableStringify(formDisplay.config) : null;
|
|
||||||
// }, [rowData]);
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// if (formDisplay) handleLoadCurrentRow();
|
|
||||||
// setRowCountInfo(null);
|
|
||||||
// handleLoadRowCount();
|
|
||||||
// }, [reloadToken]);
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// if (!formDisplay.isLoadedCorrectly) return;
|
|
||||||
|
|
||||||
// if (
|
|
||||||
// formDisplay &&
|
|
||||||
// (!formDisplay.isLoadedCurrentRow(rowData) ||
|
|
||||||
// loadedFiltersRef.current != stableStringify(formDisplay.config.filters))
|
|
||||||
// ) {
|
|
||||||
// handleLoadCurrentRow();
|
|
||||||
// }
|
|
||||||
// setRowCountInfo(null);
|
|
||||||
// handleLoadRowCount();
|
|
||||||
// }, [formDisplay]);
|
|
||||||
|
|
||||||
const reload = () => {
|
|
||||||
setLoadProps({
|
|
||||||
isLoadingData: false,
|
|
||||||
isLoadedData: false,
|
|
||||||
isLoadingCount: false,
|
|
||||||
isLoadedCount: false,
|
|
||||||
rowData: null,
|
|
||||||
loadedTime: new Date().getTime(),
|
|
||||||
allRowCount: null,
|
|
||||||
rowCountBefore: null,
|
|
||||||
errorMessage: null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (props.masterLoadedTime && props.masterLoadedTime > loadedTime) {
|
|
||||||
formDisplay.reload();
|
|
||||||
}
|
|
||||||
if (formDisplay.cache.refreshTime > loadedTime) {
|
|
||||||
reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const former = React.useMemo(() => new ChangeSetFormer(rowData, changeSetState, dispatchChangeSet, formDisplay), [
|
|
||||||
rowData,
|
|
||||||
changeSetState,
|
|
||||||
dispatchChangeSet,
|
|
||||||
formDisplay,
|
|
||||||
]);
|
|
||||||
|
|
||||||
function handleSave() {
|
|
||||||
const script = changeSetToSql(changeSetRef.current, formDisplay.dbinfo);
|
|
||||||
const sql = scriptToSql(formDisplay.driver, script);
|
|
||||||
setConfirmSql(sql);
|
|
||||||
confirmSqlModalState.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleConfirmSql() {
|
|
||||||
const resp = await axios.request({
|
|
||||||
url: 'database-connections/query-data',
|
|
||||||
method: 'post',
|
|
||||||
params: {
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
},
|
|
||||||
data: { sql: confirmSql },
|
|
||||||
});
|
|
||||||
const { errorMessage } = resp.data || {};
|
|
||||||
if (errorMessage) {
|
|
||||||
showModal(modalState => (
|
|
||||||
<ErrorMessageModal modalState={modalState} message={errorMessage} title="Error when saving" />
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
|
||||||
setConfirmSql(null);
|
|
||||||
formDisplay.reload();
|
|
||||||
// setReloadToken((x) => x + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// const { config, setConfig, cache, setCache, schemaName, pureName, conid, database } = props;
|
|
||||||
// const { formViewKey } = config;
|
|
||||||
|
|
||||||
// const [display, setDisplay] = React.useState(null);
|
|
||||||
|
|
||||||
// const connection = useConnectionInfo({ conid });
|
|
||||||
// const dbinfo = useDatabaseInfo({ conid, database });
|
|
||||||
// const extensions = useExtensions();
|
|
||||||
|
|
||||||
// console.log('SqlFormView.props', props);
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// const newDisplay = connection
|
|
||||||
// ? new TableFormViewDisplay(
|
|
||||||
// { schemaName, pureName },
|
|
||||||
// findEngineDriver(connection, extensions),
|
|
||||||
// config,
|
|
||||||
// setConfig,
|
|
||||||
// cache,
|
|
||||||
// setCache,
|
|
||||||
// dbinfo
|
|
||||||
// )
|
|
||||||
// : null;
|
|
||||||
// if (!newDisplay) return;
|
|
||||||
// if (display && display.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return;
|
|
||||||
// setDisplay(newDisplay);
|
|
||||||
// }, [config, cache, conid, database, schemaName, pureName, dbinfo, extensions]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FormView
|
|
||||||
{...props}
|
|
||||||
rowData={rowData}
|
|
||||||
onNavigate={handleNavigate}
|
|
||||||
former={former}
|
|
||||||
onSave={handleSave}
|
|
||||||
isLoading={isLoadingData}
|
|
||||||
onReload={() => formDisplay.reload()}
|
|
||||||
onReconnect={async () => {
|
|
||||||
await axios.post('database-connections/refresh', { conid, database });
|
|
||||||
formDisplay.reload();
|
|
||||||
}}
|
|
||||||
allRowCount={allRowCount}
|
|
||||||
rowCountBefore={rowCountBefore}
|
|
||||||
/>
|
|
||||||
<ConfirmSqlModal
|
|
||||||
modalState={confirmSqlModalState}
|
|
||||||
sql={confirmSql}
|
|
||||||
engine={formDisplay.engine}
|
|
||||||
onConfirm={handleConfirmSql}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export default function openReferenceForm(rowData, column, openNewTab, conid, database) {
|
|
||||||
const formViewKey = _.fromPairs(
|
|
||||||
column.foreignKey.columns.map(({ refColumnName, columnName }) => [refColumnName, rowData[columnName]])
|
|
||||||
);
|
|
||||||
openNewTab(
|
|
||||||
{
|
|
||||||
title: column.foreignKey.refTableName,
|
|
||||||
icon: 'img table',
|
|
||||||
tabComponent: 'TableDataTab',
|
|
||||||
props: {
|
|
||||||
schemaName: column.foreignKey.refSchemaName,
|
|
||||||
pureName: column.foreignKey.refTableName,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
objectTypeField: 'tables',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
grid: {
|
|
||||||
isFormView: true,
|
|
||||||
formViewKey,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
forceNewTab: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import keycodes from '../utility/keycodes';
|
|
||||||
|
|
||||||
const Row = styled.div`
|
|
||||||
// margin-left: 5px;
|
|
||||||
// margin-right: 5px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
// padding: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
background-color: ${props => props.theme.manager_background_blue[1]};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const Name = styled.div`
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 5px;
|
|
||||||
`;
|
|
||||||
const Buttons = styled.div`
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
const Icon = styled(FontIcon)`
|
|
||||||
// margin-left: 5px;
|
|
||||||
position: relative;
|
|
||||||
top: 5px;
|
|
||||||
&:hover {
|
|
||||||
background-color: ${props => props.theme.manager_background3};
|
|
||||||
}
|
|
||||||
padding: 5px;
|
|
||||||
`;
|
|
||||||
const EditorInput = styled.input`
|
|
||||||
width: calc(100% - 10px);
|
|
||||||
background-color: ${props =>
|
|
||||||
// @ts-ignore
|
|
||||||
props.isError ? props.theme.manager_background_red[1] : props.theme.manager_background};
|
|
||||||
`;
|
|
||||||
|
|
||||||
function ColumnNameEditor({
|
|
||||||
onEnter,
|
|
||||||
onBlur = undefined,
|
|
||||||
focusOnCreate = false,
|
|
||||||
blurOnEnter = false,
|
|
||||||
existingNames,
|
|
||||||
defaultValue = '',
|
|
||||||
...other
|
|
||||||
}) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const [value, setValue] = React.useState(defaultValue || '');
|
|
||||||
const editorRef = React.useRef(null);
|
|
||||||
const isError = value && existingNames && existingNames.includes(value);
|
|
||||||
const handleKeyDown = event => {
|
|
||||||
if (value && event.keyCode == keycodes.enter && !isError) {
|
|
||||||
onEnter(value);
|
|
||||||
setValue('');
|
|
||||||
if (blurOnEnter) editorRef.current.blur();
|
|
||||||
}
|
|
||||||
if (event.keyCode == keycodes.escape) {
|
|
||||||
setValue('');
|
|
||||||
editorRef.current.blur();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleBlur = () => {
|
|
||||||
if (value && !isError) {
|
|
||||||
onEnter(value);
|
|
||||||
setValue('');
|
|
||||||
}
|
|
||||||
if (onBlur) onBlur();
|
|
||||||
};
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (focusOnCreate) editorRef.current.focus();
|
|
||||||
}, [focusOnCreate]);
|
|
||||||
return (
|
|
||||||
<EditorInput
|
|
||||||
theme={theme}
|
|
||||||
ref={editorRef}
|
|
||||||
type="text"
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
value={value}
|
|
||||||
onChange={ev => setValue(ev.target.value)}
|
|
||||||
// @ts-ignore
|
|
||||||
isError={isError}
|
|
||||||
{...other}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exchange(array, i1, i2) {
|
|
||||||
const i1r = (i1 + array.length) % array.length;
|
|
||||||
const i2r = (i2 + array.length) % array.length;
|
|
||||||
const res = [...array];
|
|
||||||
[res[i1r], res[i2r]] = [res[i2r], res[i1r]];
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ColumnManagerRow({ column, onEdit, onRemove, onUp, onDown }) {
|
|
||||||
const [isHover, setIsHover] = React.useState(false);
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<Row onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)} theme={theme}>
|
|
||||||
<Name>{column.columnName}</Name>
|
|
||||||
<Buttons>
|
|
||||||
<Icon icon="icon edit" onClick={onEdit} theme={theme} />
|
|
||||||
<Icon icon="icon delete" onClick={onRemove} theme={theme} />
|
|
||||||
<Icon icon="icon arrow-up" onClick={onUp} theme={theme} />
|
|
||||||
<Icon icon="icon arrow-down" onClick={onDown} theme={theme} />
|
|
||||||
</Buttons>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dispatchChangeColumns(props, func, rowFunc = null) {
|
|
||||||
const { modelState, dispatchModel } = props;
|
|
||||||
const model = modelState.value;
|
|
||||||
|
|
||||||
dispatchModel({
|
|
||||||
type: 'set',
|
|
||||||
value: {
|
|
||||||
rows: rowFunc ? model.rows.map(rowFunc) : model.rows,
|
|
||||||
structure: {
|
|
||||||
...model.structure,
|
|
||||||
columns: func(model.structure.columns),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FreeTableColumnEditor(props) {
|
|
||||||
const { modelState, dispatchModel } = props;
|
|
||||||
const [editingColumn, setEditingColumn] = React.useState(null);
|
|
||||||
const model = modelState.value;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
|
|
||||||
{model.structure.columns.map((column, index) =>
|
|
||||||
index == editingColumn ? (
|
|
||||||
<ColumnNameEditor
|
|
||||||
defaultValue={column.columnName}
|
|
||||||
onEnter={columnName => {
|
|
||||||
dispatchChangeColumns(
|
|
||||||
props,
|
|
||||||
cols => cols.map((col, i) => (index == i ? { columnName } : col)),
|
|
||||||
row => _.mapKeys(row, (v, k) => (k == column.columnName ? columnName : k))
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onBlur={() => setEditingColumn(null)}
|
|
||||||
focusOnCreate
|
|
||||||
blurOnEnter
|
|
||||||
existingNames={model.structure.columns.map(x => x.columnName)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ColumnManagerRow
|
|
||||||
key={column.uniqueName}
|
|
||||||
column={column}
|
|
||||||
onEdit={() => setEditingColumn(index)}
|
|
||||||
onRemove={() => {
|
|
||||||
dispatchChangeColumns(props, cols => cols.filter((c, i) => i != index));
|
|
||||||
}}
|
|
||||||
onUp={() => {
|
|
||||||
dispatchChangeColumns(props, cols => exchange(cols, index, index - 1));
|
|
||||||
}}
|
|
||||||
onDown={() => {
|
|
||||||
dispatchChangeColumns(props, cols => exchange(cols, index, index + 1));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<ColumnNameEditor
|
|
||||||
onEnter={columnName => {
|
|
||||||
dispatchChangeColumns(props, cols => [...cols, { columnName }]);
|
|
||||||
}}
|
|
||||||
placeholder="New column"
|
|
||||||
existingNames={model.structure.columns.map(x => x.columnName)}
|
|
||||||
/>
|
|
||||||
</ManagerInnerContainer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { runMacro } from 'dbgate-datalib';
|
|
||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { HorizontalSplitter, VerticalSplitter } from '../widgets/Splitter';
|
|
||||||
import FreeTableColumnEditor from './FreeTableColumnEditor';
|
|
||||||
import FreeTableGridCore from './FreeTableGridCore';
|
|
||||||
import MacroDetail from './MacroDetail';
|
|
||||||
import MacroManager from './MacroManager';
|
|
||||||
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const LeftContainer = styled.div`
|
|
||||||
background-color: ${props => props.theme.manager_background};
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DataGridContainer = styled.div`
|
|
||||||
position: relative;
|
|
||||||
flex-grow: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function extractMacroValuesForMacro(macroValues, macro) {
|
|
||||||
if (!macro) return {};
|
|
||||||
return {
|
|
||||||
..._.fromPairs((macro.args || []).filter(x => x.default != null).map(x => [x.name, x.default])),
|
|
||||||
..._.mapKeys(macroValues, (v, k) => k.replace(/^.*#/, '')),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FreeTableGrid(props) {
|
|
||||||
const { modelState, dispatchModel } = props;
|
|
||||||
const theme = useTheme();
|
|
||||||
const [managerSize, setManagerSize] = React.useState(0);
|
|
||||||
const [selectedMacro, setSelectedMacro] = React.useState(null);
|
|
||||||
const [macroValues, setMacroValues] = React.useState({});
|
|
||||||
const [selectedCells, setSelectedCells] = React.useState([]);
|
|
||||||
const handleExecuteMacro = () => {
|
|
||||||
const newModel = runMacro(
|
|
||||||
selectedMacro,
|
|
||||||
extractMacroValuesForMacro(macroValues, selectedMacro),
|
|
||||||
modelState.value,
|
|
||||||
false,
|
|
||||||
selectedCells
|
|
||||||
);
|
|
||||||
dispatchModel({ type: 'set', value: newModel });
|
|
||||||
setSelectedMacro(null);
|
|
||||||
};
|
|
||||||
// console.log('macroValues', macroValues);
|
|
||||||
return (
|
|
||||||
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
|
|
||||||
<LeftContainer theme={theme}>
|
|
||||||
<WidgetColumnBar>
|
|
||||||
<WidgetColumnBarItem title="Columns" name="columns" height="40%">
|
|
||||||
<FreeTableColumnEditor {...props} />
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
<WidgetColumnBarItem title="Macros" name="macros">
|
|
||||||
<MacroManager
|
|
||||||
{...props}
|
|
||||||
managerSize={managerSize}
|
|
||||||
selectedMacro={selectedMacro}
|
|
||||||
setSelectedMacro={setSelectedMacro}
|
|
||||||
/>
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
</WidgetColumnBar>
|
|
||||||
</LeftContainer>
|
|
||||||
|
|
||||||
<DataGridContainer>
|
|
||||||
<VerticalSplitter initialValue="70%">
|
|
||||||
<FreeTableGridCore
|
|
||||||
{...props}
|
|
||||||
macroPreview={selectedMacro}
|
|
||||||
macroValues={extractMacroValuesForMacro(macroValues, selectedMacro)}
|
|
||||||
onSelectionChanged={setSelectedCells}
|
|
||||||
setSelectedMacro={setSelectedMacro}
|
|
||||||
/>
|
|
||||||
{!!selectedMacro && (
|
|
||||||
<MacroDetail
|
|
||||||
selectedMacro={selectedMacro}
|
|
||||||
setSelectedMacro={setSelectedMacro}
|
|
||||||
onChangeValues={setMacroValues}
|
|
||||||
macroValues={macroValues}
|
|
||||||
onExecute={handleExecuteMacro}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</VerticalSplitter>
|
|
||||||
</DataGridContainer>
|
|
||||||
</HorizontalSplitter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { createGridCache, FreeTableGridDisplay } from 'dbgate-datalib';
|
|
||||||
import React from 'react';
|
|
||||||
import DataGridCore from '../datagrid/DataGridCore';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import keycodes from '../utility/keycodes';
|
|
||||||
import FreeTableGrider from './FreeTableGrider';
|
|
||||||
import MacroPreviewGrider from './MacroPreviewGrider';
|
|
||||||
import uuidv1 from 'uuid/v1';
|
|
||||||
import ImportExportModal from '../modals/ImportExportModal';
|
|
||||||
|
|
||||||
export default function FreeTableGridCore(props) {
|
|
||||||
const {
|
|
||||||
modelState,
|
|
||||||
dispatchModel,
|
|
||||||
config,
|
|
||||||
setConfig,
|
|
||||||
macroPreview,
|
|
||||||
macroValues,
|
|
||||||
onSelectionChanged,
|
|
||||||
setSelectedMacro,
|
|
||||||
} = props;
|
|
||||||
const [cache, setCache] = React.useState(createGridCache());
|
|
||||||
const [selectedCells, setSelectedCells] = React.useState([]);
|
|
||||||
const showModal = useShowModal();
|
|
||||||
const grider = React.useMemo(
|
|
||||||
() =>
|
|
||||||
macroPreview
|
|
||||||
? new MacroPreviewGrider(modelState.value, macroPreview, macroValues, selectedCells)
|
|
||||||
: FreeTableGrider.factory(props),
|
|
||||||
[
|
|
||||||
...FreeTableGrider.factoryDeps(props),
|
|
||||||
macroPreview,
|
|
||||||
macroPreview ? macroValues : null,
|
|
||||||
macroPreview ? selectedCells : null,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
const display = React.useMemo(
|
|
||||||
() => new FreeTableGridDisplay(grider.model || modelState.value, config, setConfig, cache, setCache),
|
|
||||||
[modelState.value, config, cache, grider]
|
|
||||||
);
|
|
||||||
|
|
||||||
async function exportGrid() {
|
|
||||||
const jslid = uuidv1();
|
|
||||||
await axios.post('jsldata/save-free-table', { jslid, data: modelState.value });
|
|
||||||
const initialValues = {};
|
|
||||||
initialValues.sourceStorageType = 'jsldata';
|
|
||||||
initialValues.sourceJslId = jslid;
|
|
||||||
initialValues.sourceList = ['editor-data'];
|
|
||||||
showModal(modalState => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectionChanged = React.useCallback(
|
|
||||||
cells => {
|
|
||||||
if (onSelectionChanged) onSelectionChanged(cells);
|
|
||||||
setSelectedCells(cells);
|
|
||||||
},
|
|
||||||
[setSelectedCells]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(event => {
|
|
||||||
if (event.keyCode == keycodes.escape) {
|
|
||||||
setSelectedMacro(null);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataGridCore
|
|
||||||
{...props}
|
|
||||||
grider={grider}
|
|
||||||
display={display}
|
|
||||||
onSelectionChanged={macroPreview ? handleSelectionChanged : null}
|
|
||||||
frameSelection={!!macroPreview}
|
|
||||||
exportGrid={exportGrid}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { FreeTableModel } from 'dbgate-datalib';
|
|
||||||
import Grider, { GriderRowStatus } from '../datagrid/Grider';
|
|
||||||
|
|
||||||
export default class FreeTableGrider extends Grider {
|
|
||||||
public model: FreeTableModel;
|
|
||||||
private batchModel: FreeTableModel;
|
|
||||||
|
|
||||||
constructor(public modelState, public dispatchModel) {
|
|
||||||
super();
|
|
||||||
this.model = modelState && modelState.value;
|
|
||||||
}
|
|
||||||
getRowData(index: any) {
|
|
||||||
return this.model.rows[index];
|
|
||||||
}
|
|
||||||
get rowCount() {
|
|
||||||
return this.model.rows.length;
|
|
||||||
}
|
|
||||||
get currentModel(): FreeTableModel {
|
|
||||||
return this.batchModel || this.model;
|
|
||||||
}
|
|
||||||
set currentModel(value) {
|
|
||||||
if (this.batchModel) this.batchModel = value;
|
|
||||||
else this.dispatchModel({ type: 'set', value });
|
|
||||||
}
|
|
||||||
setCellValue(index: number, uniqueName: string, value: any) {
|
|
||||||
const model = this.currentModel;
|
|
||||||
if (model.rows[index])
|
|
||||||
this.currentModel = {
|
|
||||||
...model,
|
|
||||||
rows: model.rows.map((row, i) => (index == i ? { ...row, [uniqueName]: value } : row)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
get editable() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
get canInsert() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
get allowSave() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
insertRow(): number {
|
|
||||||
const model = this.currentModel;
|
|
||||||
this.currentModel = {
|
|
||||||
...model,
|
|
||||||
rows: [...model.rows, {}],
|
|
||||||
};
|
|
||||||
return this.currentModel.rows.length - 1;
|
|
||||||
}
|
|
||||||
deleteRow(index: number) {
|
|
||||||
const model = this.currentModel;
|
|
||||||
this.currentModel = {
|
|
||||||
...model,
|
|
||||||
rows: model.rows.filter((row, i) => index != i),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
beginUpdate() {
|
|
||||||
this.batchModel = this.model;
|
|
||||||
}
|
|
||||||
endUpdate() {
|
|
||||||
if (this.model != this.batchModel) {
|
|
||||||
this.dispatchModel({ type: 'set', value: this.batchModel });
|
|
||||||
this.batchModel = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static factory({ modelState, dispatchModel }): FreeTableGrider {
|
|
||||||
return new FreeTableGrider(modelState, dispatchModel);
|
|
||||||
}
|
|
||||||
static factoryDeps({ modelState, dispatchModel }) {
|
|
||||||
return [modelState, dispatchModel];
|
|
||||||
}
|
|
||||||
undo() {
|
|
||||||
this.dispatchModel({ type: 'undo' });
|
|
||||||
}
|
|
||||||
redo() {
|
|
||||||
this.dispatchModel({ type: 'redo' });
|
|
||||||
}
|
|
||||||
get canUndo() {
|
|
||||||
return this.modelState.canUndo;
|
|
||||||
}
|
|
||||||
get canRedo() {
|
|
||||||
return this.modelState.canRedo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ToolbarButton from '../widgets/ToolbarButton';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { TabPage, TabControl } from '../widgets/TabControl';
|
|
||||||
import dimensions from '../theme/dimensions';
|
|
||||||
import GenericEditor from '../sqleditor/GenericEditor';
|
|
||||||
import MacroParameters from './MacroParameters';
|
|
||||||
import { WidgetTitle } from '../widgets/WidgetStyles';
|
|
||||||
import { FormButton } from '../utility/forms';
|
|
||||||
import FormStyledButton from '../widgets/FormStyledButton';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background: ${props => props.theme.gridheader_background_cyan[0]};
|
|
||||||
height: ${dimensions.toolBar.height}px;
|
|
||||||
min-height: ${dimensions.toolBar.height}px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-top: 1px solid ${props => props.theme.border};
|
|
||||||
border-bottom: 1px solid ${props => props.theme.border};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Header = styled.div`
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 10px;
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HeaderText = styled.div`
|
|
||||||
margin-left: 10px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MacroDetailContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MacroDetailTabWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
overflow-y: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MacroSection = styled.div`
|
|
||||||
margin: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TextWrapper = styled.div`
|
|
||||||
margin: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Buttons = styled.div`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function MacroHeader({ selectedMacro, setSelectedMacro, onExecute }) {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<Container theme={theme}>
|
|
||||||
<Header>
|
|
||||||
<FontIcon icon="img macro" />
|
|
||||||
<HeaderText>{selectedMacro.title}</HeaderText>
|
|
||||||
</Header>
|
|
||||||
<Buttons>
|
|
||||||
<ToolbarButton icon="icon run" onClick={onExecute} patchY={6}>
|
|
||||||
Execute
|
|
||||||
</ToolbarButton>
|
|
||||||
<ToolbarButton icon="icon close" onClick={() => setSelectedMacro(null)} patchY={6}>
|
|
||||||
Close
|
|
||||||
</ToolbarButton>
|
|
||||||
</Buttons>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MacroDetail({ selectedMacro, setSelectedMacro, onChangeValues, macroValues, onExecute }) {
|
|
||||||
return (
|
|
||||||
<MacroDetailContainer>
|
|
||||||
<MacroHeader selectedMacro={selectedMacro} setSelectedMacro={setSelectedMacro} onExecute={onExecute} />
|
|
||||||
<TabControl>
|
|
||||||
<TabPage label="Macro detail" key="detail">
|
|
||||||
<MacroDetailTabWrapper>
|
|
||||||
<MacroSection>
|
|
||||||
<WidgetTitle>Execute</WidgetTitle>
|
|
||||||
<FormStyledButton value="Execute" onClick={onExecute} />
|
|
||||||
</MacroSection>
|
|
||||||
|
|
||||||
<MacroSection>
|
|
||||||
<WidgetTitle>Parameters</WidgetTitle>
|
|
||||||
{selectedMacro.args && selectedMacro.args.length > 0 ? (
|
|
||||||
<MacroParameters
|
|
||||||
key={selectedMacro.name}
|
|
||||||
args={selectedMacro.args}
|
|
||||||
onChangeValues={onChangeValues}
|
|
||||||
macroValues={macroValues}
|
|
||||||
namePrefix={`${selectedMacro.name}#`}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextWrapper>This macro has no parameters</TextWrapper>
|
|
||||||
)}
|
|
||||||
</MacroSection>
|
|
||||||
<MacroSection>
|
|
||||||
<WidgetTitle>Description</WidgetTitle>
|
|
||||||
<TextWrapper>{selectedMacro.description}</TextWrapper>
|
|
||||||
</MacroSection>
|
|
||||||
</MacroDetailTabWrapper>
|
|
||||||
</TabPage>
|
|
||||||
<TabPage label="JavaScript" key="javascript">
|
|
||||||
<GenericEditor readOnly value={selectedMacro.code} mode="javascript" />
|
|
||||||
</TabPage>
|
|
||||||
</TabControl>
|
|
||||||
</MacroDetailContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
|
|
||||||
import SearchInput from '../widgets/SearchInput';
|
|
||||||
import { WidgetTitle } from '../widgets/WidgetStyles';
|
|
||||||
import macros from './macros';
|
|
||||||
import { AppObjectList } from '../appobj/AppObjectList';
|
|
||||||
import MacroAppObject from '../appobj/MacroAppObject';
|
|
||||||
|
|
||||||
const SearchBoxWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function MacroManager({ managerSize, selectedMacro, setSelectedMacro }) {
|
|
||||||
const [filter, setFilter] = React.useState('');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
|
|
||||||
<SearchBoxWrapper>
|
|
||||||
<SearchInput placeholder="Search macros" filter={filter} setFilter={setFilter} />
|
|
||||||
</SearchBoxWrapper>
|
|
||||||
<AppObjectList
|
|
||||||
list={_.sortBy(macros, 'title')}
|
|
||||||
AppObjectComponent={MacroAppObject}
|
|
||||||
onObjectClick={macro => setSelectedMacro(macro)}
|
|
||||||
getCommonProps={data => ({
|
|
||||||
isBold: selectedMacro && selectedMacro.name == data.name,
|
|
||||||
})}
|
|
||||||
filter={filter}
|
|
||||||
groupFunc={data => data.group}
|
|
||||||
/>
|
|
||||||
{/* {macros.map((macro) => (
|
|
||||||
<MacroListItem key={`${macro.group}/${macro.name}`} macro={macro} />
|
|
||||||
))} */}
|
|
||||||
</ManagerInnerContainer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import FormArgumentList from '../utility/FormArgumentList';
|
|
||||||
import { FormProvider } from '../utility/FormProvider';
|
|
||||||
|
|
||||||
export default function MacroParameters({ args, onChangeValues, macroValues, namePrefix }) {
|
|
||||||
if (!args || args.length == 0) return null;
|
|
||||||
const initialValues = {
|
|
||||||
..._.fromPairs(args.filter(x => x.default != null).map(x => [`${namePrefix}${x.name}`, x.default])),
|
|
||||||
...macroValues,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<FormProvider initialValues={initialValues}>
|
|
||||||
<FormArgumentList args={args} onChangeValues={onChangeValues} namePrefix={namePrefix} />
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { FreeTableModel, MacroDefinition, MacroSelectedCell, runMacro } from 'dbgate-datalib';
|
|
||||||
import Grider, { GriderRowStatus } from '../datagrid/Grider';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
function convertToSet(row, field) {
|
|
||||||
if (!row) return null;
|
|
||||||
if (!row[field]) return null;
|
|
||||||
if (_.isSet(row[field])) return row[field];
|
|
||||||
return new Set(row[field]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MacroPreviewGrider extends Grider {
|
|
||||||
model: FreeTableModel;
|
|
||||||
_errors: string[] = [];
|
|
||||||
constructor(model: FreeTableModel, macro: MacroDefinition, macroArgs: {}, selectedCells: MacroSelectedCell[]) {
|
|
||||||
super();
|
|
||||||
this.model = runMacro(macro, macroArgs, model, true, selectedCells, this._errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
get errors() {
|
|
||||||
return this._errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRowStatus(index): GriderRowStatus {
|
|
||||||
const row = this.model.rows[index];
|
|
||||||
return {
|
|
||||||
status: (row && row.__rowStatus) || 'regular',
|
|
||||||
modifiedFields: convertToSet(row, '__modifiedFields'),
|
|
||||||
insertedFields: convertToSet(row, '__insertedFields'),
|
|
||||||
deletedFields: convertToSet(row, '__deletedFields'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getRowData(index: any) {
|
|
||||||
return this.model.rows[index];
|
|
||||||
}
|
|
||||||
get rowCount() {
|
|
||||||
return this.model.rows.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
const macros = [
|
|
||||||
{
|
|
||||||
title: 'Remove diacritics',
|
|
||||||
name: 'removeDiacritics',
|
|
||||||
group: 'Text',
|
|
||||||
description: 'Removes diacritics from selected cells',
|
|
||||||
type: 'transformValue',
|
|
||||||
code: `return modules.lodash.deburr(value)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Search & replace text',
|
|
||||||
name: 'stringReplace',
|
|
||||||
group: 'Text',
|
|
||||||
description: 'Search & replace text or regular expression',
|
|
||||||
type: 'transformValue',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Find',
|
|
||||||
name: 'find',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Replace with',
|
|
||||||
name: 'replace',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'checkbox',
|
|
||||||
label: 'Case sensitive',
|
|
||||||
name: 'caseSensitive',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'checkbox',
|
|
||||||
label: 'Regular expression',
|
|
||||||
name: 'isRegex',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
code: `
|
|
||||||
const rtext = args.isRegex ? args.find : modules.lodash.escapeRegExp(args.find);
|
|
||||||
const rflags = args.caseSensitive ? 'g' : 'ig';
|
|
||||||
return value ? value.toString().replace(new RegExp(rtext, rflags), args.replace || '') : value
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Change text case',
|
|
||||||
name: 'changeTextCase',
|
|
||||||
group: 'Text',
|
|
||||||
description: 'Uppercase, lowercase and other case functions',
|
|
||||||
type: 'transformValue',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
options: ['toUpper', 'toLower', 'lowerCase', 'upperCase', 'kebabCase', 'snakeCase', 'camelCase', 'startCase'],
|
|
||||||
label: 'Type',
|
|
||||||
name: 'type',
|
|
||||||
default: 'toUpper',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
code: `return modules.lodash[args.type](value)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Row index',
|
|
||||||
name: 'rowIndex',
|
|
||||||
group: 'Tools',
|
|
||||||
description: 'Index of row from 1 (autoincrement)',
|
|
||||||
type: 'transformValue',
|
|
||||||
code: `return rowIndex + 1`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Generate UUID',
|
|
||||||
name: 'uuidv1',
|
|
||||||
group: 'Tools',
|
|
||||||
description: 'Generate unique identifier',
|
|
||||||
type: 'transformValue',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ value: 'uuidv1', name: 'V1 - from timestamp' },
|
|
||||||
{ value: 'uuidv4', name: 'V4 - random generated' },
|
|
||||||
],
|
|
||||||
label: 'Version',
|
|
||||||
name: 'version',
|
|
||||||
default: 'uuidv1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
code: `return modules[args.version]()`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Current date',
|
|
||||||
name: 'currentDate',
|
|
||||||
group: 'Tools',
|
|
||||||
description: 'Gets current date',
|
|
||||||
type: 'transformValue',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Format',
|
|
||||||
name: 'format',
|
|
||||||
default: 'YYYY-MM-DD HH:mm:ss',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
code: `return modules.moment().format(args.format)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Duplicate rows',
|
|
||||||
name: 'duplicateRows',
|
|
||||||
group: 'Tools',
|
|
||||||
description: 'Duplicate selected rows',
|
|
||||||
type: 'transformRows',
|
|
||||||
code: `
|
|
||||||
const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
|
|
||||||
const selectedRows = modules.lodash.groupBy(selectedCells, 'row');
|
|
||||||
const maxIndex = modules.lodash.max(selectedRowIndexes);
|
|
||||||
return [
|
|
||||||
...rows.slice(0, maxIndex + 1),
|
|
||||||
...selectedRowIndexes.map(index => ({
|
|
||||||
...modules.lodash.pick(rows[index], selectedRows[index].map(x => x.column)),
|
|
||||||
__rowStatus: 'inserted',
|
|
||||||
})),
|
|
||||||
...rows.slice(maxIndex + 1),
|
|
||||||
]
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Delete empty rows',
|
|
||||||
name: 'deleteEmptyRows',
|
|
||||||
group: 'Tools',
|
|
||||||
description: 'Delete empty rows - rows with all values null or empty string',
|
|
||||||
type: 'transformRows',
|
|
||||||
code: `
|
|
||||||
return rows.map(row => {
|
|
||||||
if (cols.find(col => row[col])) return row;
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
__rowStatus: 'deleted',
|
|
||||||
};
|
|
||||||
})
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Duplicate columns',
|
|
||||||
name: 'duplicateColumns',
|
|
||||||
group: 'Tools',
|
|
||||||
description: 'Duplicate selected columns',
|
|
||||||
type: 'transformData',
|
|
||||||
code: `
|
|
||||||
const selectedColumnNames = modules.lodash.uniq(selectedCells.map(x => x.column));
|
|
||||||
const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
|
|
||||||
const addedColumnNames = selectedColumnNames.map(col => (args.prefix || '') + col + (args.postfix || ''));
|
|
||||||
const resultRows = rows.map((row, rowIndex) => ({
|
|
||||||
...row,
|
|
||||||
...(selectedRowIndexes.includes(rowIndex) ? modules.lodash.fromPairs(selectedColumnNames.map(col => [(args.prefix || '') + col + (args.postfix || ''), row[col]])) : {}),
|
|
||||||
__insertedFields: addedColumnNames,
|
|
||||||
}));
|
|
||||||
const resultCols = [
|
|
||||||
...cols,
|
|
||||||
...addedColumnNames,
|
|
||||||
];
|
|
||||||
return {
|
|
||||||
rows: resultRows,
|
|
||||||
cols: resultCols,
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Prefix',
|
|
||||||
name: 'prefix',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Postfix',
|
|
||||||
name: 'postfix',
|
|
||||||
default: '_copy',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Extract date fields',
|
|
||||||
name: 'extractDateFields',
|
|
||||||
group: 'Tools',
|
|
||||||
description: 'Extract yaear, month, day and other date/time fields from selection and adds it as new columns',
|
|
||||||
type: 'transformData',
|
|
||||||
code: `
|
|
||||||
const selectedColumnNames = modules.lodash.uniq(selectedCells.map(x => x.column));
|
|
||||||
const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
|
|
||||||
const addedColumnNames = modules.lodash.compact([args.year, args.month, args.day, args.hour, args.minute, args.second]);
|
|
||||||
const selectedRows = modules.lodash.groupBy(selectedCells, 'row');
|
|
||||||
const resultRows = rows.map((row, rowIndex) => {
|
|
||||||
if (!selectedRowIndexes.includes(rowIndex)) return {
|
|
||||||
...row,
|
|
||||||
__insertedFields: addedColumnNames,
|
|
||||||
};
|
|
||||||
let mom = null;
|
|
||||||
for(const cell of selectedRows[rowIndex]) {
|
|
||||||
const m = modules.moment(row[cell.column]);
|
|
||||||
if (m.isValid()) {
|
|
||||||
mom = m;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!mom) return {
|
|
||||||
...row,
|
|
||||||
__insertedFields: addedColumnNames,
|
|
||||||
};
|
|
||||||
|
|
||||||
const fields = {
|
|
||||||
[args.year]: mom.year(),
|
|
||||||
[args.month]: mom.month() + 1,
|
|
||||||
[args.day]: mom.day(),
|
|
||||||
[args.hour]: mom.hour(),
|
|
||||||
[args.minute]: mom.minute(),
|
|
||||||
[args.second]: mom.second(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
...modules.lodash.pick(fields, addedColumnNames),
|
|
||||||
__insertedFields: addedColumnNames,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const resultCols = [
|
|
||||||
...cols,
|
|
||||||
...addedColumnNames,
|
|
||||||
];
|
|
||||||
return {
|
|
||||||
rows: resultRows,
|
|
||||||
cols: resultCols,
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Year name',
|
|
||||||
name: 'year',
|
|
||||||
default: 'year',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Month name',
|
|
||||||
name: 'month',
|
|
||||||
default: 'month',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Day name',
|
|
||||||
name: 'day',
|
|
||||||
default: 'day',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Hour name',
|
|
||||||
name: 'hour',
|
|
||||||
default: 'hour',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Minute name',
|
|
||||||
name: 'minute',
|
|
||||||
default: 'minute',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
label: 'Second name',
|
|
||||||
name: 'second',
|
|
||||||
default: 'second',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default macros;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
|
|
||||||
export default function useNewFreeTable() {
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
|
|
||||||
return ({ title = undefined, ...props } = {}) =>
|
|
||||||
openNewTab({
|
|
||||||
title: title || 'Data #',
|
|
||||||
icon: 'img free-table',
|
|
||||||
tabComponent: 'FreeTableTab',
|
|
||||||
props,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const iconNames = {
|
|
||||||
'icon minus-box': 'mdi mdi-minus-box-outline',
|
|
||||||
'icon plus-box': 'mdi mdi-plus-box-outline',
|
|
||||||
'icon invisible-box': 'mdi mdi-minus-box-outline icon-invisible',
|
|
||||||
'icon cloud-upload': 'mdi mdi-cloud-upload',
|
|
||||||
'icon import': 'mdi mdi-application-import',
|
|
||||||
'icon export': 'mdi mdi-application-export',
|
|
||||||
'icon new-connection': 'mdi mdi-database-plus',
|
|
||||||
'icon tables': 'mdi mdi-table-multiple',
|
|
||||||
'icon favorite': 'mdi mdi-star',
|
|
||||||
'icon share': 'mdi mdi-share-variant',
|
|
||||||
'icon add': 'mdi mdi-plus-circle',
|
|
||||||
'icon connection': 'mdi mdi-connection',
|
|
||||||
|
|
||||||
'icon database': 'mdi mdi-database',
|
|
||||||
'icon server': 'mdi mdi-server',
|
|
||||||
'icon table': 'mdi mdi-table',
|
|
||||||
'icon archive': 'mdi mdi-archive',
|
|
||||||
'icon file': 'mdi mdi-file',
|
|
||||||
'icon loading': 'mdi mdi-loading mdi-spin',
|
|
||||||
'icon close': 'mdi mdi-close',
|
|
||||||
'icon filter': 'mdi mdi-filter',
|
|
||||||
'icon filter-off': 'mdi mdi-filter-off',
|
|
||||||
'icon reload': 'mdi mdi-reload',
|
|
||||||
'icon undo': 'mdi mdi-undo',
|
|
||||||
'icon redo': 'mdi mdi-redo',
|
|
||||||
'icon save': 'mdi mdi-content-save',
|
|
||||||
'icon account': 'mdi mdi-account',
|
|
||||||
'icon sql-file': 'mdi mdi-file',
|
|
||||||
'icon web': 'mdi mdi-web',
|
|
||||||
'icon home': 'mdi mdi-home',
|
|
||||||
'icon query-design': 'mdi mdi-vector-polyline-edit',
|
|
||||||
'icon form': 'mdi mdi-form-select',
|
|
||||||
|
|
||||||
'icon edit': 'mdi mdi-pencil',
|
|
||||||
'icon delete': 'mdi mdi-delete',
|
|
||||||
'icon arrow-up': 'mdi mdi-arrow-up',
|
|
||||||
'icon arrow-down': 'mdi mdi-arrow-down',
|
|
||||||
'icon arrow-left': 'mdi mdi-arrow-left',
|
|
||||||
'icon arrow-begin': 'mdi mdi-arrow-collapse-left',
|
|
||||||
'icon arrow-end': 'mdi mdi-arrow-collapse-right',
|
|
||||||
'icon arrow-right': 'mdi mdi-arrow-right',
|
|
||||||
'icon format-code': 'mdi mdi-code-tags-check',
|
|
||||||
'icon show-wizard': 'mdi mdi-comment-edit',
|
|
||||||
'icon disconnected': 'mdi mdi-lan-disconnect',
|
|
||||||
'icon theme': 'mdi mdi-brightness-6',
|
|
||||||
'icon error': 'mdi mdi-close-circle',
|
|
||||||
'icon ok': 'mdi mdi-check-circle',
|
|
||||||
'icon markdown': 'mdi mdi-application',
|
|
||||||
'icon preview': 'mdi mdi-file-find',
|
|
||||||
'icon eye': 'mdi mdi-eye',
|
|
||||||
|
|
||||||
'icon run': 'mdi mdi-play',
|
|
||||||
'icon chevron-down': 'mdi mdi-chevron-down',
|
|
||||||
'icon chevron-left': 'mdi mdi-chevron-left',
|
|
||||||
'icon chevron-right': 'mdi mdi-chevron-right',
|
|
||||||
'icon chevron-up': 'mdi mdi-chevron-up',
|
|
||||||
'icon plugin': 'mdi mdi-toy-brick',
|
|
||||||
|
|
||||||
'img ok': 'mdi mdi-check-circle color-green-8',
|
|
||||||
'img alert': 'mdi mdi-alert-circle color-blue-6',
|
|
||||||
'img error': 'mdi mdi-close-circle color-red-7',
|
|
||||||
'img warn': 'mdi mdi-alert color-gold-7',
|
|
||||||
// 'img statusbar-ok': 'mdi mdi-check-circle color-on-statusbar-green',
|
|
||||||
|
|
||||||
'img archive': 'mdi mdi-table color-gold-7',
|
|
||||||
'img archive-folder': 'mdi mdi-database-outline color-green-7',
|
|
||||||
'img autoincrement': 'mdi mdi-numeric-1-box-multiple-outline',
|
|
||||||
'img column': 'mdi mdi-table-column',
|
|
||||||
'img server': 'mdi mdi-server color-blue-7',
|
|
||||||
'img primary-key': 'mdi mdi-key-star color-yellow-7',
|
|
||||||
'img foreign-key': 'mdi mdi-key-link',
|
|
||||||
'img sql-file': 'mdi mdi-file',
|
|
||||||
'img shell': 'mdi mdi-flash color-blue-7',
|
|
||||||
'img chart': 'mdi mdi-chart-bar color-magenta-7',
|
|
||||||
'img markdown': 'mdi mdi-application color-red-7',
|
|
||||||
'img preview': 'mdi mdi-file-find color-red-7',
|
|
||||||
'img favorite': 'mdi mdi-star color-yellow-7',
|
|
||||||
'img query-design': 'mdi mdi-vector-polyline-edit color-red-7',
|
|
||||||
|
|
||||||
'img free-table': 'mdi mdi-table color-green-7',
|
|
||||||
'img macro': 'mdi mdi-hammer-wrench',
|
|
||||||
|
|
||||||
'img database': 'mdi mdi-database color-gold-7',
|
|
||||||
'img table': 'mdi mdi-table color-blue-7',
|
|
||||||
'img view': 'mdi mdi-table color-magenta-7',
|
|
||||||
'img procedure': 'mdi mdi-cog color-blue-7',
|
|
||||||
'img function': 'mdi mdi-function-variant',
|
|
||||||
|
|
||||||
'img sort-asc': 'mdi mdi-sort-alphabetical-ascending color-green',
|
|
||||||
'img sort-desc': 'mdi mdi-sort-alphabetical-descending color-green',
|
|
||||||
|
|
||||||
'img reference': 'mdi mdi-link-box',
|
|
||||||
'img link': 'mdi mdi-link',
|
|
||||||
'img filter': 'mdi mdi-filter',
|
|
||||||
'img group': 'mdi mdi-group',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FontIcon({ icon, className = '', ...other }) {
|
|
||||||
if (!icon) return null;
|
|
||||||
let cls = icon;
|
|
||||||
if (icon.startsWith('icon ') || icon.startsWith('img ')) {
|
|
||||||
cls = iconNames[icon];
|
|
||||||
if (!cls) return null;
|
|
||||||
}
|
|
||||||
return <span className={`${cls} ${className}`} {...other} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ExpandIcon({ isBlank = false, isExpanded = false, ...other }) {
|
|
||||||
if (isBlank) {
|
|
||||||
return <FontIcon icon="icon invisible-box" {...other} />;
|
|
||||||
}
|
|
||||||
return <FontIcon icon={isExpanded ? 'icon minus-box' : 'icon plus-box'} {...other} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChevronExpandIcon({ isBlank = false, isExpanded = false, ...other }) {
|
|
||||||
if (isBlank) {
|
|
||||||
return <FontIcon icon="icon invisible-box" {...other} />;
|
|
||||||
}
|
|
||||||
return <FontIcon icon={isExpanded ? 'icon chevron-down' : 'icon chevron-right'} {...other} />;
|
|
||||||
}
|
|
||||||
@@ -1,580 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import FormStyledButton from '../widgets/FormStyledButton';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import {
|
|
||||||
FormReactSelect,
|
|
||||||
FormConnectionSelect,
|
|
||||||
FormDatabaseSelect,
|
|
||||||
FormTablesSelect,
|
|
||||||
FormSchemaSelect,
|
|
||||||
FormArchiveFolderSelect,
|
|
||||||
FormArchiveFilesSelect,
|
|
||||||
} from '../utility/forms';
|
|
||||||
import { useArchiveFiles, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
|
||||||
import TableControl, { TableColumn } from '../utility/TableControl';
|
|
||||||
import { TextField, SelectField, CheckboxField } from '../utility/inputs';
|
|
||||||
import { createPreviewReader, getActionOptions, getTargetName } from './createImpExpScript';
|
|
||||||
import getElectron from '../utility/getElectron';
|
|
||||||
import ErrorInfo from '../widgets/ErrorInfo';
|
|
||||||
import getAsArray from '../utility/getAsArray';
|
|
||||||
import LoadingInfo from '../widgets/LoadingInfo';
|
|
||||||
import SqlEditor from '../sqleditor/SqlEditor';
|
|
||||||
import { useUploadsProvider } from '../utility/UploadsProvider';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import { findFileFormat, getFileFormatDirections } from '../utility/fileformats';
|
|
||||||
import FormArgumentList from '../utility/FormArgumentList';
|
|
||||||
import useExtensions from '../utility/useExtensions';
|
|
||||||
import UploadButton from '../utility/UploadButton';
|
|
||||||
import useShowModal from '../modals/showModal';
|
|
||||||
import ChangeDownloadUrlModal from '../modals/ChangeDownloadUrlModal';
|
|
||||||
import { useForm } from '../utility/FormProvider';
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
// max-height: 50vh;
|
|
||||||
// overflow-y: scroll;
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SourceListWrapper = styled.div`
|
|
||||||
margin: 10px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Column = styled.div`
|
|
||||||
margin: 10px;
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Label = styled.div`
|
|
||||||
margin: 5px;
|
|
||||||
margin-top: 15px;
|
|
||||||
color: ${props => props.theme.modal_font2};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SourceNameWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SourceNameButtons = styled.div`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const IconButtonWrapper = styled.div`
|
|
||||||
&:hover {
|
|
||||||
background-color: ${props => props.theme.modal_background2};
|
|
||||||
}
|
|
||||||
cursor: pointer;
|
|
||||||
color: ${props => props.theme.modal_font_blue[7]};
|
|
||||||
margin-left: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SqlWrapper = styled.div`
|
|
||||||
position: relative;
|
|
||||||
height: 100px;
|
|
||||||
width: 20vw;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DragWrapper = styled.div`
|
|
||||||
padding: 10px;
|
|
||||||
background: ${props => props.theme.modal_background2};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ArrowWrapper = styled.div`
|
|
||||||
font-size: 30px;
|
|
||||||
color: ${props => props.theme.modal_font_blue[7]};
|
|
||||||
align-self: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Title = styled.div`
|
|
||||||
font-size: 20px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 10px 0px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ButtonsLine = styled.div`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function getFileFilters(extensions, storageType) {
|
|
||||||
const res = [];
|
|
||||||
const format = findFileFormat(extensions, storageType);
|
|
||||||
if (format) res.push({ name: format.name, extensions: [format.extension] });
|
|
||||||
res.push({ name: 'All Files', extensions: ['*'] });
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addFileToSourceListDefault({ fileName, shortName, isDownload }, newSources, newValues) {
|
|
||||||
const sourceName = shortName;
|
|
||||||
newSources.push(sourceName);
|
|
||||||
newValues[`sourceFile_${sourceName}`] = {
|
|
||||||
fileName,
|
|
||||||
isDownload,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addFilesToSourceList(extensions, files, values, setValues, preferedStorageType, setPreviewSource) {
|
|
||||||
const newSources = [];
|
|
||||||
const newValues = {};
|
|
||||||
const storage = preferedStorageType || values.sourceStorageType;
|
|
||||||
for (const file of getAsArray(files)) {
|
|
||||||
const format = findFileFormat(extensions, storage);
|
|
||||||
if (format) {
|
|
||||||
await (format.addFileToSourceList || addFileToSourceListDefault)(file, newSources, newValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newValues['sourceList'] = [...(values.sourceList || []).filter(x => !newSources.includes(x)), ...newSources];
|
|
||||||
if (preferedStorageType && preferedStorageType != values.sourceStorageType) {
|
|
||||||
newValues['sourceStorageType'] = preferedStorageType;
|
|
||||||
}
|
|
||||||
setValues({
|
|
||||||
...values,
|
|
||||||
...newValues,
|
|
||||||
});
|
|
||||||
if (setPreviewSource && newSources.length == 1) {
|
|
||||||
setPreviewSource(newSources[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ElectronFilesInput() {
|
|
||||||
const { values, setValues } = useForm();
|
|
||||||
const electron = getElectron();
|
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
|
||||||
const extensions = useExtensions();
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
|
||||||
const files = electron.remote.dialog.showOpenDialogSync(electron.remote.getCurrentWindow(), {
|
|
||||||
properties: ['openFile', 'multiSelections'],
|
|
||||||
filters: getFileFilters(extensions, values.sourceStorageType),
|
|
||||||
});
|
|
||||||
if (files) {
|
|
||||||
const path = window.require('path');
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
await addFilesToSourceList(
|
|
||||||
extensions,
|
|
||||||
files.map(full => ({
|
|
||||||
fileName: full,
|
|
||||||
shortName: path.parse(full).name,
|
|
||||||
})),
|
|
||||||
values,
|
|
||||||
setValues
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FormStyledButton type="button" value="Add file(s)" onClick={handleClick} />
|
|
||||||
{isLoading && <LoadingInfo message="Anaysing input files" />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractUrlName(url, values) {
|
|
||||||
const match = url.match(/\/([^/]+)($|\?)/);
|
|
||||||
if (match) {
|
|
||||||
const res = match[1];
|
|
||||||
if (res.includes('.')) {
|
|
||||||
return res.slice(0, res.indexOf('.'));
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
return `url${values && values.sourceList ? values.sourceList.length + 1 : '1'}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilesInput({ setPreviewSource = undefined }) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const electron = getElectron();
|
|
||||||
const showModal = useShowModal();
|
|
||||||
const { values, setValues } = useForm();
|
|
||||||
const extensions = useExtensions();
|
|
||||||
const doAddUrl = url => {
|
|
||||||
addFilesToSourceList(
|
|
||||||
extensions,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
fileName: url,
|
|
||||||
shortName: extractUrlName(url, values),
|
|
||||||
isDownload: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
values,
|
|
||||||
setValues,
|
|
||||||
null,
|
|
||||||
setPreviewSource
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const handleAddUrl = () =>
|
|
||||||
showModal(modalState => <ChangeDownloadUrlModal modalState={modalState} onConfirm={doAddUrl} />);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ButtonsLine>
|
|
||||||
{electron ? <ElectronFilesInput /> : <UploadButton />}
|
|
||||||
<FormStyledButton value="Add web URL" onClick={handleAddUrl} />
|
|
||||||
</ButtonsLine>
|
|
||||||
<DragWrapper theme={theme}>Drag & drop imported files here</DragWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SourceTargetConfig({
|
|
||||||
direction,
|
|
||||||
storageTypeField,
|
|
||||||
connectionIdField,
|
|
||||||
databaseNameField,
|
|
||||||
archiveFolderField,
|
|
||||||
schemaNameField,
|
|
||||||
tablesField = undefined,
|
|
||||||
engine = undefined,
|
|
||||||
setPreviewSource = undefined,
|
|
||||||
}) {
|
|
||||||
const extensions = useExtensions();
|
|
||||||
const theme = useTheme();
|
|
||||||
const { values, setFieldValue } = useForm();
|
|
||||||
const types =
|
|
||||||
values[storageTypeField] == 'jsldata'
|
|
||||||
? [{ value: 'jsldata', label: 'Query result data', directions: ['source'] }]
|
|
||||||
: [
|
|
||||||
{ value: 'database', label: 'Database', directions: ['source', 'target'] },
|
|
||||||
...extensions.fileFormats.map(format => ({
|
|
||||||
value: format.storageType,
|
|
||||||
label: `${format.name} files(s)`,
|
|
||||||
directions: getFileFormatDirections(format),
|
|
||||||
})),
|
|
||||||
{ value: 'query', label: 'SQL Query', directions: ['source'] },
|
|
||||||
{ value: 'archive', label: 'Archive', directions: ['source', 'target'] },
|
|
||||||
];
|
|
||||||
const storageType = values[storageTypeField];
|
|
||||||
const dbinfo = useDatabaseInfo({ conid: values[connectionIdField], database: values[databaseNameField] });
|
|
||||||
const archiveFiles = useArchiveFiles({ folder: values[archiveFolderField] });
|
|
||||||
const format = findFileFormat(extensions, storageType);
|
|
||||||
return (
|
|
||||||
<Column>
|
|
||||||
{direction == 'source' && (
|
|
||||||
<Title theme={theme}>
|
|
||||||
<FontIcon icon="icon import" /> Source configuration
|
|
||||||
</Title>
|
|
||||||
)}
|
|
||||||
{direction == 'target' && (
|
|
||||||
<Title theme={theme}>
|
|
||||||
<FontIcon icon="icon export" /> Target configuration
|
|
||||||
</Title>
|
|
||||||
)}
|
|
||||||
<FormReactSelect options={types.filter(x => x.directions.includes(direction))} name={storageTypeField} />
|
|
||||||
{(storageType == 'database' || storageType == 'query') && (
|
|
||||||
<>
|
|
||||||
<Label theme={theme}>Server</Label>
|
|
||||||
<FormConnectionSelect name={connectionIdField} />
|
|
||||||
<Label theme={theme}>Database</Label>
|
|
||||||
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{storageType == 'database' && (
|
|
||||||
<>
|
|
||||||
<Label theme={theme}>Schema</Label>
|
|
||||||
<FormSchemaSelect conidName={connectionIdField} databaseName={databaseNameField} name={schemaNameField} />
|
|
||||||
{tablesField && (
|
|
||||||
<>
|
|
||||||
<Label theme={theme}>Tables/views</Label>
|
|
||||||
<FormTablesSelect
|
|
||||||
conidName={connectionIdField}
|
|
||||||
schemaName={schemaNameField}
|
|
||||||
databaseName={databaseNameField}
|
|
||||||
name={tablesField}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<FormStyledButton
|
|
||||||
type="button"
|
|
||||||
value="All tables"
|
|
||||||
onClick={() =>
|
|
||||||
setFieldValue(
|
|
||||||
'sourceList',
|
|
||||||
_.uniq([...(values.sourceList || []), ...(dbinfo && dbinfo.tables.map(x => x.pureName))])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FormStyledButton
|
|
||||||
type="button"
|
|
||||||
value="All views"
|
|
||||||
onClick={() =>
|
|
||||||
setFieldValue(
|
|
||||||
'sourceList',
|
|
||||||
_.uniq([...(values.sourceList || []), ...(dbinfo && dbinfo.views.map(x => x.pureName))])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FormStyledButton type="button" value="Remove all" onClick={() => setFieldValue('sourceList', [])} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{storageType == 'query' && (
|
|
||||||
<>
|
|
||||||
<Label theme={theme}>Query</Label>
|
|
||||||
<SqlWrapper>
|
|
||||||
<SqlEditor
|
|
||||||
value={values.sourceSql}
|
|
||||||
onChange={value => setFieldValue('sourceSql', value)}
|
|
||||||
engine={engine}
|
|
||||||
focusOnCreate
|
|
||||||
/>
|
|
||||||
</SqlWrapper>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{storageType == 'archive' && (
|
|
||||||
<>
|
|
||||||
<Label theme={theme}>Archive folder</Label>
|
|
||||||
<FormArchiveFolderSelect
|
|
||||||
name={archiveFolderField}
|
|
||||||
additionalFolders={_.compact([values[archiveFolderField]])}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{storageType == 'archive' && direction == 'source' && (
|
|
||||||
<>
|
|
||||||
<Label theme={theme}>Source files</Label>
|
|
||||||
<FormArchiveFilesSelect folderName={values[archiveFolderField]} name={tablesField} />
|
|
||||||
<div>
|
|
||||||
<FormStyledButton
|
|
||||||
type="button"
|
|
||||||
value="All files"
|
|
||||||
onClick={() =>
|
|
||||||
setFieldValue(
|
|
||||||
'sourceList',
|
|
||||||
_.uniq([...(values.sourceList || []), ...(archiveFiles && archiveFiles.map(x => x.name))])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FormStyledButton type="button" value="Remove all" onClick={() => setFieldValue('sourceList', [])} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!format && direction == 'source' && <FilesInput setPreviewSource={setPreviewSource} />}
|
|
||||||
|
|
||||||
{format && format.args && (
|
|
||||||
<FormArgumentList
|
|
||||||
args={format.args.filter(arg => !arg.direction || arg.direction == direction)}
|
|
||||||
namePrefix={`${direction}_${format.storageType}_`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SourceName({ name }) {
|
|
||||||
const { values, setFieldValue } = useForm();
|
|
||||||
const theme = useTheme();
|
|
||||||
const showModal = useShowModal();
|
|
||||||
const obj = values[`sourceFile_${name}`];
|
|
||||||
const handleDelete = () => {
|
|
||||||
setFieldValue(
|
|
||||||
'sourceList',
|
|
||||||
values.sourceList.filter(x => x != name)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const doChangeUrl = url => {
|
|
||||||
setFieldValue(`sourceFile_${name}`, { fileName: url, isDownload: true });
|
|
||||||
};
|
|
||||||
const handleChangeUrl = () => {
|
|
||||||
showModal(modalState => (
|
|
||||||
<ChangeDownloadUrlModal modalState={modalState} url={obj.fileName} onConfirm={doChangeUrl} />
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SourceNameWrapper>
|
|
||||||
<div>{name}</div>
|
|
||||||
<SourceNameButtons>
|
|
||||||
{obj && !!obj.isDownload && (
|
|
||||||
<IconButtonWrapper onClick={handleChangeUrl} theme={theme} title={obj && obj.fileName}>
|
|
||||||
<FontIcon icon="icon web" />
|
|
||||||
</IconButtonWrapper>
|
|
||||||
)}
|
|
||||||
<IconButtonWrapper onClick={handleDelete} theme={theme}>
|
|
||||||
<FontIcon icon="icon delete" />
|
|
||||||
</IconButtonWrapper>
|
|
||||||
</SourceNameButtons>
|
|
||||||
</SourceNameWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ImportExportConfigurator({
|
|
||||||
uploadedFile = undefined,
|
|
||||||
openedFile = undefined,
|
|
||||||
onChangePreview = undefined,
|
|
||||||
}) {
|
|
||||||
const { values, setFieldValue, setValues } = useForm();
|
|
||||||
const targetDbinfo = useDatabaseInfo({ conid: values.targetConnectionId, database: values.targetDatabaseName });
|
|
||||||
const sourceConnectionInfo = useConnectionInfo({ conid: values.sourceConnectionId });
|
|
||||||
const { engine: sourceEngine } = sourceConnectionInfo || {};
|
|
||||||
const { sourceList } = values;
|
|
||||||
const { setUploadListener } = useUploadsProvider();
|
|
||||||
const theme = useTheme();
|
|
||||||
const [previewSource, setPreviewSource] = React.useState(null);
|
|
||||||
const extensions = useExtensions();
|
|
||||||
|
|
||||||
const handleUpload = React.useCallback(
|
|
||||||
file => {
|
|
||||||
addFilesToSourceList(
|
|
||||||
extensions,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
fileName: file.filePath,
|
|
||||||
shortName: file.shortName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
values,
|
|
||||||
setValues,
|
|
||||||
!sourceList || sourceList.length == 0 ? file.storageType : null,
|
|
||||||
setPreviewSource
|
|
||||||
);
|
|
||||||
// setFieldValue('sourceList', [...(sourceList || []), file.originalName]);
|
|
||||||
},
|
|
||||||
[extensions, setFieldValue, sourceList, values]
|
|
||||||
);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setUploadListener(() => handleUpload);
|
|
||||||
return () => {
|
|
||||||
setUploadListener(null);
|
|
||||||
};
|
|
||||||
}, [handleUpload]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (uploadedFile) {
|
|
||||||
handleUpload(uploadedFile);
|
|
||||||
}
|
|
||||||
if (openedFile) {
|
|
||||||
addFilesToSourceList(
|
|
||||||
extensions,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
fileName: openedFile.filePath,
|
|
||||||
shortName: openedFile.shortName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
values,
|
|
||||||
setValues,
|
|
||||||
!sourceList || sourceList.length == 0 ? openedFile.storageType : null,
|
|
||||||
setPreviewSource
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const supportsPreview =
|
|
||||||
!!findFileFormat(extensions, values.sourceStorageType) || values.sourceStorageType == 'archive';
|
|
||||||
const previewFileName =
|
|
||||||
previewSource && values[`sourceFile_${previewSource}`] && values[`sourceFile_${previewSource}`].fileName;
|
|
||||||
|
|
||||||
const handleChangePreviewSource = async () => {
|
|
||||||
if (previewSource && supportsPreview) {
|
|
||||||
const reader = await createPreviewReader(extensions, values, previewSource);
|
|
||||||
if (onChangePreview) onChangePreview(reader);
|
|
||||||
} else {
|
|
||||||
onChangePreview(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
handleChangePreviewSource();
|
|
||||||
}, [previewSource, supportsPreview, previewFileName]);
|
|
||||||
|
|
||||||
const oldValues = React.useRef({});
|
|
||||||
React.useEffect(() => {
|
|
||||||
const changed = _.pickBy(
|
|
||||||
values,
|
|
||||||
(v, k) => k.startsWith(`source_${values.sourceStorageType}_`) && oldValues.current[k] != v
|
|
||||||
);
|
|
||||||
if (!_.isEmpty(changed)) {
|
|
||||||
handleChangePreviewSource();
|
|
||||||
}
|
|
||||||
oldValues.current = values;
|
|
||||||
}, [values]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Wrapper>
|
|
||||||
<SourceTargetConfig
|
|
||||||
direction="source"
|
|
||||||
storageTypeField="sourceStorageType"
|
|
||||||
connectionIdField="sourceConnectionId"
|
|
||||||
databaseNameField="sourceDatabaseName"
|
|
||||||
archiveFolderField="sourceArchiveFolder"
|
|
||||||
schemaNameField="sourceSchemaName"
|
|
||||||
tablesField="sourceList"
|
|
||||||
engine={sourceEngine}
|
|
||||||
setPreviewSource={setPreviewSource}
|
|
||||||
/>
|
|
||||||
<ArrowWrapper theme={theme}>
|
|
||||||
<FontIcon icon="icon arrow-right" />
|
|
||||||
</ArrowWrapper>
|
|
||||||
<SourceTargetConfig
|
|
||||||
direction="target"
|
|
||||||
storageTypeField="targetStorageType"
|
|
||||||
connectionIdField="targetConnectionId"
|
|
||||||
databaseNameField="targetDatabaseName"
|
|
||||||
archiveFolderField="targetArchiveFolder"
|
|
||||||
schemaNameField="targetSchemaName"
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
<SourceListWrapper>
|
|
||||||
<Title>
|
|
||||||
<FontIcon icon="icon tables" /> Map source tables/files
|
|
||||||
</Title>
|
|
||||||
<TableControl rows={sourceList || []}>
|
|
||||||
<TableColumn fieldName="source" header="Source" formatter={row => <SourceName name={row} />} />
|
|
||||||
<TableColumn
|
|
||||||
fieldName="action"
|
|
||||||
header="Action"
|
|
||||||
formatter={row => (
|
|
||||||
<SelectField
|
|
||||||
options={getActionOptions(extensions, row, values, targetDbinfo)}
|
|
||||||
value={values[`actionType_${row}`] || getActionOptions(extensions, row, values, targetDbinfo)[0].value}
|
|
||||||
onChange={e => setFieldValue(`actionType_${row}`, e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<TableColumn
|
|
||||||
fieldName="target"
|
|
||||||
header="Target"
|
|
||||||
formatter={row => (
|
|
||||||
<TextField
|
|
||||||
value={getTargetName(extensions, row, values)}
|
|
||||||
onChange={e => setFieldValue(`targetName_${row}`, e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<TableColumn
|
|
||||||
fieldName="preview"
|
|
||||||
header="Preview"
|
|
||||||
formatter={row =>
|
|
||||||
supportsPreview ? (
|
|
||||||
<CheckboxField
|
|
||||||
checked={previewSource == row}
|
|
||||||
onChange={e => {
|
|
||||||
if (e.target.checked) setPreviewSource(row);
|
|
||||||
else setPreviewSource(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableControl>
|
|
||||||
{(sourceList || []).length == 0 && <ErrorInfo message="No source tables/files" icon="img alert" />}
|
|
||||||
</SourceListWrapper>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { createGridCache, createGridConfig, FreeTableGridDisplay } from 'dbgate-datalib';
|
|
||||||
import React from 'react';
|
|
||||||
import DataGridCore from '../datagrid/DataGridCore';
|
|
||||||
import RowsArrayGrider from '../datagrid/RowsArrayGrider';
|
|
||||||
import axios from '../utility/axios';
|
|
||||||
import ErrorInfo from '../widgets/ErrorInfo';
|
|
||||||
import LoadingInfo from '../widgets/LoadingInfo';
|
|
||||||
|
|
||||||
export default function PreviewDataGrid({ reader, ...other }) {
|
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
|
||||||
const [errorMessage, setErrorMessage] = React.useState(null);
|
|
||||||
const [model, setModel] = React.useState(null);
|
|
||||||
const [config, setConfig] = React.useState(createGridConfig());
|
|
||||||
const [cache, setCache] = React.useState(createGridCache());
|
|
||||||
const [grider, setGrider] = React.useState(null);
|
|
||||||
|
|
||||||
const handleLoadInitialData = async () => {
|
|
||||||
try {
|
|
||||||
if (!reader) {
|
|
||||||
setModel(null);
|
|
||||||
setGrider(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setErrorMessage(null);
|
|
||||||
setIsLoading(true);
|
|
||||||
const resp = await axios.post('runners/load-reader', reader);
|
|
||||||
// @ts-ignore
|
|
||||||
setModel(resp.data);
|
|
||||||
setGrider(new RowsArrayGrider(resp.data.rows));
|
|
||||||
setIsLoading(false);
|
|
||||||
} catch (err) {
|
|
||||||
setIsLoading(false);
|
|
||||||
const errorMessage = (err && err.response && err.response.data && err.response.data.error) || 'Loading failed';
|
|
||||||
setErrorMessage(errorMessage);
|
|
||||||
console.error(err.response);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
handleLoadInitialData();
|
|
||||||
}, [reader]);
|
|
||||||
|
|
||||||
const display = React.useMemo(() => new FreeTableGridDisplay(model, config, setConfig, cache, setCache), [
|
|
||||||
model,
|
|
||||||
config,
|
|
||||||
cache,
|
|
||||||
grider,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <LoadingInfo wrapper message="Loading data" />;
|
|
||||||
}
|
|
||||||
if (errorMessage) {
|
|
||||||
return <ErrorInfo message={errorMessage} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!grider) return null;
|
|
||||||
|
|
||||||
return <DataGridCore {...other} grider={grider} display={display} />;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { extractShellApiFunctionName, extractShellApiPlugins } from 'dbgate-tools';
|
|
||||||
|
|
||||||
export default class ScriptWriter {
|
|
||||||
constructor(varCount = '0') {
|
|
||||||
this.s = '';
|
|
||||||
this.packageNames = [];
|
|
||||||
// this.engines = [];
|
|
||||||
this.varCount = parseInt(varCount) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
allocVariable(prefix = 'var') {
|
|
||||||
this.varCount += 1;
|
|
||||||
return `${prefix}${this.varCount}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
put(s = '') {
|
|
||||||
this.s += s;
|
|
||||||
this.s += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
assign(variableName, functionName, props) {
|
|
||||||
this.put(`const ${variableName} = await ${extractShellApiFunctionName(functionName)}(${JSON.stringify(props)});`);
|
|
||||||
this.packageNames.push(...extractShellApiPlugins(functionName, props));
|
|
||||||
}
|
|
||||||
|
|
||||||
requirePackage(packageName) {
|
|
||||||
this.packageNames.push(packageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyStream(sourceVar, targetVar) {
|
|
||||||
this.put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
comment(s) {
|
|
||||||
this.put(`// ${s}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getScript(schedule = null) {
|
|
||||||
const packageNames = this.packageNames;
|
|
||||||
let prefix = _.uniq(packageNames)
|
|
||||||
.map(packageName => `// @require ${packageName}\n`)
|
|
||||||
.join('');
|
|
||||||
if (schedule) prefix += `// @schedule ${schedule}`;
|
|
||||||
if (prefix) prefix += '\n';
|
|
||||||
|
|
||||||
return prefix + this.s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import ScriptWriter from './ScriptWriter';
|
|
||||||
import getAsArray from '../utility/getAsArray';
|
|
||||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
|
||||||
import { findEngineDriver, findObjectLike } from 'dbgate-tools';
|
|
||||||
import { findFileFormat } from '../utility/fileformats';
|
|
||||||
|
|
||||||
export function getTargetName(extensions, source, values) {
|
|
||||||
const key = `targetName_${source}`;
|
|
||||||
if (values[key]) return values[key];
|
|
||||||
const format = findFileFormat(extensions, values.targetStorageType);
|
|
||||||
if (format) {
|
|
||||||
const res = format.getDefaultOutputName ? format.getDefaultOutputName(source, values) : null;
|
|
||||||
if (res) return res;
|
|
||||||
return `${source}.${format.extension}`;
|
|
||||||
}
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractApiParameters(values, direction, format) {
|
|
||||||
const pairs = (format.args || [])
|
|
||||||
.filter(arg => arg.apiName)
|
|
||||||
.map(arg => [arg.apiName, values[`${direction}_${format.storageType}_${arg.name}`]])
|
|
||||||
.filter(x => x[1] != null);
|
|
||||||
return _.fromPairs(pairs);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getConnection(extensions, storageType, conid, database) {
|
|
||||||
if (storageType == 'database' || storageType == 'query') {
|
|
||||||
const conn = await getConnectionInfo({ conid });
|
|
||||||
const driver = findEngineDriver(conn, extensions);
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
..._.omit(conn, ['_id', 'displayName']),
|
|
||||||
database,
|
|
||||||
},
|
|
||||||
driver,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [null, null];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSourceExpr(extensions, sourceName, values, sourceConnection, sourceDriver) {
|
|
||||||
const { sourceStorageType } = values;
|
|
||||||
if (sourceStorageType == 'database') {
|
|
||||||
const fullName = { schemaName: values.sourceSchemaName, pureName: sourceName };
|
|
||||||
return [
|
|
||||||
'tableReader',
|
|
||||||
{
|
|
||||||
connection: sourceConnection,
|
|
||||||
...fullName,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (sourceStorageType == 'query') {
|
|
||||||
return [
|
|
||||||
'queryReader',
|
|
||||||
{
|
|
||||||
connection: sourceConnection,
|
|
||||||
sql: values.sourceSql,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (findFileFormat(extensions, sourceStorageType)) {
|
|
||||||
const sourceFile = values[`sourceFile_${sourceName}`];
|
|
||||||
const format = findFileFormat(extensions, sourceStorageType);
|
|
||||||
if (format && format.readerFunc) {
|
|
||||||
return [
|
|
||||||
format.readerFunc,
|
|
||||||
{
|
|
||||||
..._.omit(sourceFile, ['isDownload']),
|
|
||||||
...extractApiParameters(values, 'source', format),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sourceStorageType == 'jsldata') {
|
|
||||||
return ['jslDataReader', { jslid: values.sourceJslId }];
|
|
||||||
}
|
|
||||||
if (sourceStorageType == 'archive') {
|
|
||||||
return [
|
|
||||||
'archiveReader',
|
|
||||||
{
|
|
||||||
folderName: values.sourceArchiveFolder,
|
|
||||||
fileName: sourceName,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
throw new Error(`Unknown source storage type: ${sourceStorageType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFlagsFroAction(action) {
|
|
||||||
switch (action) {
|
|
||||||
case 'dropCreateTable':
|
|
||||||
return {
|
|
||||||
createIfNotExists: true,
|
|
||||||
dropIfExists: true,
|
|
||||||
};
|
|
||||||
case 'truncate':
|
|
||||||
return {
|
|
||||||
createIfNotExists: true,
|
|
||||||
truncate: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
createIfNotExists: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTargetExpr(extensions, sourceName, values, targetConnection, targetDriver) {
|
|
||||||
const { targetStorageType } = values;
|
|
||||||
const format = findFileFormat(extensions, targetStorageType);
|
|
||||||
if (format && format.writerFunc) {
|
|
||||||
const outputParams = format.getOutputParams && format.getOutputParams(sourceName, values);
|
|
||||||
return [
|
|
||||||
format.writerFunc,
|
|
||||||
{
|
|
||||||
...(outputParams
|
|
||||||
? outputParams
|
|
||||||
: {
|
|
||||||
fileName: getTargetName(extensions, sourceName, values),
|
|
||||||
}),
|
|
||||||
...extractApiParameters(values, 'target', format),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (targetStorageType == 'database') {
|
|
||||||
return [
|
|
||||||
'tableWriter',
|
|
||||||
{
|
|
||||||
connection: targetConnection,
|
|
||||||
schemaName: values.targetSchemaName,
|
|
||||||
pureName: getTargetName(extensions, sourceName, values),
|
|
||||||
...getFlagsFroAction(values[`actionType_${sourceName}`]),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (targetStorageType == 'archive') {
|
|
||||||
return [
|
|
||||||
'archiveWriter',
|
|
||||||
{
|
|
||||||
folderName: values.targetArchiveFolder,
|
|
||||||
fileName: getTargetName(extensions, sourceName, values),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unknown target storage type: ${targetStorageType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function createImpExpScript(extensions, values, addEditorInfo = true) {
|
|
||||||
const script = new ScriptWriter(values.startVariableIndex || 0);
|
|
||||||
|
|
||||||
const [sourceConnection, sourceDriver] = await getConnection(
|
|
||||||
extensions,
|
|
||||||
values.sourceStorageType,
|
|
||||||
values.sourceConnectionId,
|
|
||||||
values.sourceDatabaseName
|
|
||||||
);
|
|
||||||
const [targetConnection, targetDriver] = await getConnection(
|
|
||||||
extensions,
|
|
||||||
values.targetStorageType,
|
|
||||||
values.targetConnectionId,
|
|
||||||
values.targetDatabaseName
|
|
||||||
);
|
|
||||||
|
|
||||||
const sourceList = getAsArray(values.sourceList);
|
|
||||||
for (const sourceName of sourceList) {
|
|
||||||
const sourceVar = script.allocVariable();
|
|
||||||
// @ts-ignore
|
|
||||||
script.assign(sourceVar, ...getSourceExpr(extensions, sourceName, values, sourceConnection, sourceDriver));
|
|
||||||
|
|
||||||
const targetVar = script.allocVariable();
|
|
||||||
// @ts-ignore
|
|
||||||
script.assign(targetVar, ...getTargetExpr(extensions, sourceName, values, targetConnection, targetDriver));
|
|
||||||
|
|
||||||
script.copyStream(sourceVar, targetVar);
|
|
||||||
script.put();
|
|
||||||
}
|
|
||||||
if (addEditorInfo) {
|
|
||||||
script.comment('@ImportExportConfigurator');
|
|
||||||
script.comment(JSON.stringify(values));
|
|
||||||
}
|
|
||||||
return script.getScript(values.schedule);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getActionOptions(extensions, source, values, targetDbinfo) {
|
|
||||||
const res = [];
|
|
||||||
const targetName = getTargetName(extensions, source, values);
|
|
||||||
if (values.targetStorageType == 'database') {
|
|
||||||
let existing = findObjectLike(
|
|
||||||
{ schemaName: values.targetSchemaName, pureName: targetName },
|
|
||||||
targetDbinfo,
|
|
||||||
'tables'
|
|
||||||
);
|
|
||||||
if (existing) {
|
|
||||||
res.push({
|
|
||||||
label: 'Append data',
|
|
||||||
value: 'appendData',
|
|
||||||
});
|
|
||||||
res.push({
|
|
||||||
label: 'Truncate and import',
|
|
||||||
value: 'truncate',
|
|
||||||
});
|
|
||||||
res.push({
|
|
||||||
label: 'Drop and create table',
|
|
||||||
value: 'dropCreateTable',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.push({
|
|
||||||
label: 'Create table',
|
|
||||||
value: 'createTable',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.push({
|
|
||||||
label: 'Create file',
|
|
||||||
value: 'createFile',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createPreviewReader(extensions, values, sourceName) {
|
|
||||||
const [sourceConnection, sourceDriver] = await getConnection(
|
|
||||||
extensions,
|
|
||||||
values.sourceStorageType,
|
|
||||||
values.sourceConnectionId,
|
|
||||||
values.sourceDatabaseName
|
|
||||||
);
|
|
||||||
const [functionName, props] = getSourceExpr(extensions, sourceName, values, sourceConnection, sourceDriver);
|
|
||||||
return {
|
|
||||||
functionName,
|
|
||||||
props: {
|
|
||||||
...props,
|
|
||||||
limitRows: 100,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, HelveticaNeue-Light, Ubuntu, Droid Sans,
|
|
||||||
sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
*/
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.RactModalOverlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: #00000080;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-invisible {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.largeFormMarker input[type='text'] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.largeFormMarker input[type='password'] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.largeFormMarker select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import './index.css';
|
|
||||||
import '@mdi/font/css/materialdesignicons.css';
|
|
||||||
import App from './App';
|
|
||||||
import * as serviceWorker from './serviceWorker';
|
|
||||||
|
|
||||||
import 'ace-builds/src-noconflict/mode-sql';
|
|
||||||
import 'ace-builds/src-noconflict/mode-mysql';
|
|
||||||
import 'ace-builds/src-noconflict/mode-pgsql';
|
|
||||||
import 'ace-builds/src-noconflict/mode-sqlserver';
|
|
||||||
import 'ace-builds/src-noconflict/mode-json';
|
|
||||||
import 'ace-builds/src-noconflict/mode-javascript';
|
|
||||||
import 'ace-builds/src-noconflict/mode-markdown';
|
|
||||||
import 'ace-builds/src-noconflict/theme-github';
|
|
||||||
import 'ace-builds/src-noconflict/theme-twilight';
|
|
||||||
import 'ace-builds/src-noconflict/ext-searchbox';
|
|
||||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
|
||||||
import localStorageGarbageCollector from './utility/localStorageGarbageCollector';
|
|
||||||
// import 'ace-builds/src-noconflict/snippets/sqlserver';
|
|
||||||
// import 'ace-builds/src-noconflict/snippets/pgsql';
|
|
||||||
// import 'ace-builds/src-noconflict/snippets/mysql';
|
|
||||||
|
|
||||||
localStorageGarbageCollector();
|
|
||||||
window['dbgate_tabExports'] = {};
|
|
||||||
window['dbgate_getCurrentTabCommands'] = () => {
|
|
||||||
const tabid = window['dbgate_activeTabId'];
|
|
||||||
return _.mapValues(window['dbgate_tabExports'][tabid] || {}, v => !!v);
|
|
||||||
};
|
|
||||||
window['dbgate_tabCommand'] = cmd => {
|
|
||||||
const tabid = window['dbgate_activeTabId'];
|
|
||||||
const commands = window['dbgate_tabExports'][tabid];
|
|
||||||
const func = (commands || {})[cmd];
|
|
||||||
if (func) func();
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'));
|
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
|
||||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
|
||||||
serviceWorker.unregister();
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user