Merge pull request #65 from janproch/master

Svelte proof of concept
This commit is contained in:
Jan Prochazka
2021-02-26 20:29:32 +01:00
committed by GitHub
301 changed files with 4309 additions and 26789 deletions

View File

@@ -9,7 +9,7 @@
"start:api": "yarn workspace dbgate-api start",
"start:api:portal": "yarn workspace dbgate-api start:portal",
"start:api:covid": "yarn workspace dbgate-api start:covid",
"start:web": "yarn workspace dbgate-web start",
"start:web": "yarn workspace dbgate-web dev",
"start:sqltree": "yarn workspace dbgate-sqltree start",
"start:tools": "yarn workspace dbgate-tools start",
"start:datalib": "yarn workspace dbgate-datalib start",

View File

@@ -21,6 +21,11 @@ export interface FileFormatDefinition {
getOutputParams?: (sourceName, values) => any;
}
export interface ThemeDefinition {
className: string;
themeName: string;
}
export interface PluginDefinition {
packageName: string;
manifest: any;
@@ -31,4 +36,5 @@ export interface ExtensionsDirectory {
plugins: PluginDefinition[];
fileFormats: FileFormatDefinition[];
drivers: EngineDriver[];
themes: ThemeDefinition[];
}

View File

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

105
packages/web/README.md Normal file
View File

@@ -0,0 +1,105 @@
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
---
# svelte app
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
## Building and running in production mode
To create an optimised version of the app:
```bash
npm run build
```
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
## Single-page app mode
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
```js
"start": "sirv public --single"
```
## Using TypeScript
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
```bash
node scripts/setupTypeScript.js
```
Or remove the script via:
```bash
rm scripts/setupTypeScript.js
```
## Deploying to the web
### With [Vercel](https://vercel.com)
Install `vercel` if you haven't already:
```bash
npm install -g vercel
```
Then, from within your project folder:
```bash
cd public
vercel deploy --name my-project
```
### With [surge](https://surge.sh/)
Install `surge` if you haven't already:
```bash
npm install -g surge
```
Then, from within your project folder:
```bash
npm run build
surge public my-project.surge.sh
```

View File

@@ -1,69 +1,41 @@
{
"name": "dbgate-web",
"version": "3.9.5",
"files": [
"build"
],
"version": "1.0.0",
"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"
]
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public",
"validate": "svelte-check"
},
"devDependencies": {
"@types/react": "^16.9.17",
"@types/styled-components": "^4.4.2",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-typescript": "^6.0.0",
"@tsconfig/svelte": "^1.0.0",
"rollup": "^2.3.4",
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0",
"svelte-check": "^1.0.0",
"svelte-preprocess": "^4.0.0",
"tslib": "^2.0.0",
"typescript": "^3.9.3",
"socket.io-client": "^2.3.0",
"sql-formatter": "^2.3.3",
"uuid": "^3.4.0",
"json-stable-stringify": "^1.0.1",
"localforage": "^1.9.0",
"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"
"lodash": "^4.17.15"
},
"dependencies": {
"@mdi/font": "^5.9.55",
"sirv-cli": "^1.0.0"
}
}

419
packages/web/public/bulma.css vendored Normal file
View File

@@ -0,0 +1,419 @@
.m-0 {
margin: 0 !important;
}
.mt-0 {
margin-top: 0 !important;
}
.mr-0 {
margin-right: 0 !important;
}
.mb-0 {
margin-bottom: 0 !important;
}
.ml-0 {
margin-left: 0 !important;
}
.mx-0 {
margin-left: 0 !important;
margin-right: 0 !important;
}
.my-0 {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.m-1 {
margin: 0.25rem !important;
}
.mt-1 {
margin-top: 0.25rem !important;
}
.mr-1 {
margin-right: 0.25rem !important;
}
.mb-1 {
margin-bottom: 0.25rem !important;
}
.ml-1 {
margin-left: 0.25rem !important;
}
.mx-1 {
margin-left: 0.25rem !important;
margin-right: 0.25rem !important;
}
.my-1 {
margin-top: 0.25rem !important;
margin-bottom: 0.25rem !important;
}
.m-2 {
margin: 0.5rem !important;
}
.mt-2 {
margin-top: 0.5rem !important;
}
.mr-2 {
margin-right: 0.5rem !important;
}
.mb-2 {
margin-bottom: 0.5rem !important;
}
.ml-2 {
margin-left: 0.5rem !important;
}
.mx-2 {
margin-left: 0.5rem !important;
margin-right: 0.5rem !important;
}
.my-2 {
margin-top: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
.m-3 {
margin: 0.75rem !important;
}
.mt-3 {
margin-top: 0.75rem !important;
}
.mr-3 {
margin-right: 0.75rem !important;
}
.mb-3 {
margin-bottom: 0.75rem !important;
}
.ml-3 {
margin-left: 0.75rem !important;
}
.mx-3 {
margin-left: 0.75rem !important;
margin-right: 0.75rem !important;
}
.my-3 {
margin-top: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
.m-4 {
margin: 1rem !important;
}
.mt-4 {
margin-top: 1rem !important;
}
.mr-4 {
margin-right: 1rem !important;
}
.mb-4 {
margin-bottom: 1rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
.mx-4 {
margin-left: 1rem !important;
margin-right: 1rem !important;
}
.my-4 {
margin-top: 1rem !important;
margin-bottom: 1rem !important;
}
.m-5 {
margin: 1.5rem !important;
}
.mt-5 {
margin-top: 1.5rem !important;
}
.mr-5 {
margin-right: 1.5rem !important;
}
.mb-5 {
margin-bottom: 1.5rem !important;
}
.ml-5 {
margin-left: 1.5rem !important;
}
.mx-5 {
margin-left: 1.5rem !important;
margin-right: 1.5rem !important;
}
.my-5 {
margin-top: 1.5rem !important;
margin-bottom: 1.5rem !important;
}
.m-6 {
margin: 3rem !important;
}
.mt-6 {
margin-top: 3rem !important;
}
.mr-6 {
margin-right: 3rem !important;
}
.mb-6 {
margin-bottom: 3rem !important;
}
.ml-6 {
margin-left: 3rem !important;
}
.mx-6 {
margin-left: 3rem !important;
margin-right: 3rem !important;
}
.my-6 {
margin-top: 3rem !important;
margin-bottom: 3rem !important;
}
.p-0 {
padding: 0 !important;
}
.pt-0 {
padding-top: 0 !important;
}
.pr-0 {
padding-right: 0 !important;
}
.pb-0 {
padding-bottom: 0 !important;
}
.pl-0 {
padding-left: 0 !important;
}
.px-0 {
padding-left: 0 !important;
padding-right: 0 !important;
}
.py-0 {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.p-1 {
padding: 0.25rem !important;
}
.pt-1 {
padding-top: 0.25rem !important;
}
.pr-1 {
padding-right: 0.25rem !important;
}
.pb-1 {
padding-bottom: 0.25rem !important;
}
.pl-1 {
padding-left: 0.25rem !important;
}
.px-1 {
padding-left: 0.25rem !important;
padding-right: 0.25rem !important;
}
.py-1 {
padding-top: 0.25rem !important;
padding-bottom: 0.25rem !important;
}
.p-2 {
padding: 0.5rem !important;
}
.pt-2 {
padding-top: 0.5rem !important;
}
.pr-2 {
padding-right: 0.5rem !important;
}
.pb-2 {
padding-bottom: 0.5rem !important;
}
.pl-2 {
padding-left: 0.5rem !important;
}
.px-2 {
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
}
.py-2 {
padding-top: 0.5rem !important;
padding-bottom: 0.5rem !important;
}
.p-3 {
padding: 0.75rem !important;
}
.pt-3 {
padding-top: 0.75rem !important;
}
.pr-3 {
padding-right: 0.75rem !important;
}
.pb-3 {
padding-bottom: 0.75rem !important;
}
.pl-3 {
padding-left: 0.75rem !important;
}
.px-3 {
padding-left: 0.75rem !important;
padding-right: 0.75rem !important;
}
.py-3 {
padding-top: 0.75rem !important;
padding-bottom: 0.75rem !important;
}
.p-4 {
padding: 1rem !important;
}
.pt-4 {
padding-top: 1rem !important;
}
.pr-4 {
padding-right: 1rem !important;
}
.pb-4 {
padding-bottom: 1rem !important;
}
.pl-4 {
padding-left: 1rem !important;
}
.px-4 {
padding-left: 1rem !important;
padding-right: 1rem !important;
}
.py-4 {
padding-top: 1rem !important;
padding-bottom: 1rem !important;
}
.p-5 {
padding: 1.5rem !important;
}
.pt-5 {
padding-top: 1.5rem !important;
}
.pr-5 {
padding-right: 1.5rem !important;
}
.pb-5 {
padding-bottom: 1.5rem !important;
}
.pl-5 {
padding-left: 1.5rem !important;
}
.px-5 {
padding-left: 1.5rem !important;
padding-right: 1.5rem !important;
}
.py-5 {
padding-top: 1.5rem !important;
padding-bottom: 1.5rem !important;
}
.p-6 {
padding: 3rem !important;
}
.pt-6 {
padding-top: 3rem !important;
}
.pr-6 {
padding-right: 3rem !important;
}
.pb-6 {
padding-bottom: 3rem !important;
}
.pl-6 {
padding-left: 3rem !important;
}
.px-6 {
padding-left: 3rem !important;
padding-right: 3rem !important;
}
.py-6 {
padding-top: 3rem !important;
padding-bottom: 3rem !important;
}

View File

@@ -0,0 +1,19 @@
:root {
--dim-widget-icon-size: 50px;
--dim-statusbar-height: 20px;
--dim-left-panel-width: 300px;
--dim-tabs-panel-height: 53px;
--dim-splitter-thickness: 3px;
--dim-visible-left-panel: 1; /* set from JS */
--dim-content-left: calc(
var(--dim-widget-icon-size) + var(--dim-visible-left-panel) *
(var(--dim-left-panel-width) + var(--dim-splitter-thickness))
);
--dim-visible-toolbar: 1; /* set from JS */
--dim-toolbar-height: 30px;
--dim-header-top: calc(var(--dim-toolbar-height) * var(--dim-visible-toolbar));
--dim-content-top: calc(var(--dim-header-top) + var(--dim-tabs-panel-height));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,75 @@
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;
}
/* html, body {
position: relative;
width: 100%;
height: 100%;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
-webkit-padding: 0.4em 0;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
} */

View File

@@ -0,0 +1,23 @@
.color-icon-blue {
color: var(--theme-icon-blue);
}
.color-icon-green {
color: var(--theme-icon-green);
}
.color-icon-red {
color: var(--theme-icon-red);
}
.color-icon-gold {
color: var(--theme-icon-gold);
}
.color-icon-yellow {
color: var(--theme-icon-yellow);
}
.color-icon-magenta {
color: var(--theme-icon-magenta);
}

View File

@@ -1,44 +1,22 @@
<!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.
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
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>
<title>Svelte app</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/dimensions.css'>
<link rel='stylesheet' href='/bulma.css'>
<link rel='stylesheet' href='/icon-colors.css'>
<link rel='stylesheet' href='/build/bundle.css'>
<link rel='stylesheet' href='/build/fonts/materialdesignicons.css'>
<script defer src='/build/bundle.js'></script>
</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>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,99 @@
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import copy from 'rollup-plugin-copy';
import { terser } from 'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';
import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true,
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
},
};
}
export default {
input: 'src/main.ts',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js',
},
plugins: [
copy({
targets: [
{
src:
'../../node_modules/@mdi/font/css/materialdesignicons.css',
dest: 'public/build/fonts/',
},
{
src:
'../../node_modules/@mdi/font/fonts/*',
dest: 'public/build/fonts/',
},
],
}),
svelte({
preprocess: sveltePreprocess({ sourceMap: !production }),
compilerOptions: {
// enable run-time checks when not in production
dev: !production,
},
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: 'bundle.css' }),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte'],
}),
commonjs(),
typescript({
sourceMap: !production,
inlineSources: !production,
}),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser(),
],
watch: {
clearScreen: false,
},
};

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import CommandListener from './commands/CommandListener.svelte';
import PluginsProvider from './plugins/PluginsProvider.svelte';
import Screen from './Screen.svelte';
</script>
<PluginsProvider />
<CommandListener />
<Screen />

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,103 @@
<script lang="ts">
import WidgetContainer from './widgets/WidgetContainer.svelte';
import WidgetIconPanel from './widgets/WidgetIconPanel.svelte';
import { currentTheme, selectedWidget, visibleCommandPalette, visibleToolbar } from './stores';
import TabsPanel from './widgets/TabsPanel.svelte';
import TabContent from './TabContent.svelte';
import CommandPalette from './commands/CommandPalette.svelte';
import Toolbar from './widgets/Toolbar.svelte';
</script>
<div class={`${$currentTheme} root`}>
<div class="iconbar">
<WidgetIconPanel />
</div>
<div class="statusbar" />
{#if $selectedWidget}
<div class="leftpanel">
<WidgetContainer />
</div>
{/if}
<div class="tabs">
<TabsPanel />
</div>
<div class="content">
<TabContent />
</div>
{#if $visibleCommandPalette}
<div class="commads">
<CommandPalette />
</div>
{/if}
{#if $visibleToolbar}
<div class="toolbar">
<Toolbar />
</div>
{/if}
</div>
<style>
.root {
color: var(--theme-font-1);
}
.iconbar {
position: fixed;
left: 0;
top: var(--dim-header-top);
bottom: var(--dim-statusbar-height);
width: var(--dim-widget-icon-size);
background: var(--theme-bg-inv-1);
}
.statusbar {
position: fixed;
background: var(--theme-bg-statusbar-inv);
height: var(--dim-statusbar-height);
left: 0;
right: 0;
bottom: 0;
}
.leftpanel {
position: fixed;
top: var(--dim-header-top);
left: var(--dim-widget-icon-size);
bottom: var(--dim-statusbar-height);
width: var(--dim-left-panel-width);
background-color: var(--theme-bg-1);
display: flex;
}
.tabs {
display: flex;
position: fixed;
top: var(--dim-header-top);
left: var(--dim-content-left);
height: var(--dim-tabs-panel-height);
right: 0;
background-color: var(--theme-bg-2);
border-top: 1px solid var(--theme-border);
overflow-x: auto;
}
.tabs::-webkit-scrollbar {
height: 7px;
}
.content {
position: fixed;
top: var(--dim-content-top);
left: var(--dim-content-left);
bottom: var(--dim-statusbar-height);
right: 0;
background-color: var(--theme-bg-1);
}
.commads {
position: fixed;
top: var(--dim-header-top);
left: var(--dim-widget-icon-size);
}
.toolbar {
position: fixed;
top: 0;
height: var(--dim-toolbar-height);
left: 0;
right: 0;
}
</style>

View File

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

View File

@@ -0,0 +1,71 @@
<script context="module" lang="ts">
function createTabComponent(selectedTab) {
const tabComponent = tabs[selectedTab.tabComponent];
if (tabComponent) {
return {
tabComponent,
props: selectedTab.props,
};
}
return null;
}
</script>
<script lang="ts">
import _ from 'lodash';
import { openedTabs } from './stores';
import tabs from './tabs';
let mountedTabs = {};
$: selectedTab = $openedTabs.find(x => x.selected && x.closedTime == null);
// cleanup closed tabs
$: {
if (
_.difference(
_.keys(mountedTabs),
_.map(
$openedTabs.filter(x => x.closedTime == null),
'tabid'
)
).length > 0
) {
mountedTabs = _.pickBy(mountedTabs, (v, k) => $openedTabs.find(x => x.tabid == k && x.closedTime == null));
}
}
// open missing tabs
$: {
if (selectedTab) {
const { tabid } = selectedTab;
if (tabid && !mountedTabs[tabid])
mountedTabs = {
...mountedTabs,
[tabid]: createTabComponent(selectedTab),
};
}
}
</script>
{#each _.keys(mountedTabs) as tabid (tabid)}
<div class:tabVisible={tabid == (selectedTab && selectedTab.tabid)}>
<svelte:component this={mountedTabs[tabid].tabComponent} {...mountedTabs[tabid].props} {tabid} />
</div>
{/each}
<style>
div {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
}
.tabVisible {
visibility: visible;
}
:not(.tabVisible) {
visibility: hidden;
}
</style>

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
export let icon;
export let title;
export let isBold = false;
export let prefix = '';
export let isBusy = false;
export let statusIcon = undefined;
export let statusTitle = undefined;
export let extInfo = undefined;
</script>
<div class="main" class:isBold draggable on:click>
{prefix}
{#if isBusy}
<FontIcon icon="icon loading" />
{:else}
<FontIcon {icon} />
{/if}
{title}
{#if statusIcon}
<span class="status">
<FontIcon icon={statusIcon} title={statusTitle} />
</span>
{/if}
{#if extInfo}
<span class="ext-info">
{extInfo}
</span>
{/if}
</div>
<slot />
<style>
.main {
padding: 5px;
cursor: pointer;
white-space: nowrap;
font-weight: normal;
}
.main:hover {
background-color: var(--theme-bg-hover);
}
.isBold {
font-weight: bold;
}
.status {
margin-left: 5px;
}
.ext-info {
font-weight: normal;
margin-left: 5px;
color: var(--theme-font-3);
}
</style>

View File

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

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import AppObjectListItem from './AppObjectListItem.svelte';
export let list;
export let component;
export let subItemsComponent = undefined;
export let expandOnClick = false;
export let groupFunc = undefined;
</script>
{#each list as data}
<AppObjectListItem {component} {subItemsComponent} {expandOnClick} {data} on:objectClick />
{/each}

View File

@@ -0,0 +1,20 @@
<script lang="ts">
export let component;
export let data;
export let subItemsComponent;
export let expandOnClick;
let isExpanded = false;
function handleExpand() {
if (subItemsComponent && expandOnClick) {
isExpanded = !isExpanded;
}
}
</script>
<svelte:component this={component} {data} on:click={handleExpand} />
{#if isExpanded && subItemsComponent}
<svelte:component this={subItemsComponent} {data} />
{/if}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import _ from 'lodash';
import AppObjectCore from './AppObjectCore.svelte';
import { currentDatabase, extensions, openedConnections } from '../stores';
export let commonProps;
export let data;
let statusIcon = null;
let statusTitle = null;
let extInfo = null;
let engineStatusIcon = null;
let engineStatusTitle = null;
$: {
if ($extensions.drivers.find(x => x.engine == data.engine)) {
const match = (data.engine || '').match(/^([^@]*)@/);
extInfo = match ? match[1] : data.engine;
engineStatusIcon = null;
engineStatusTitle = null;
} else {
extInfo = data.engine;
engineStatusIcon = 'img warn';
engineStatusTitle = `Engine driver ${data.engine} not found, review installed plugins and change engine in edit connection dialog`;
}
}
$: {
const { _id, status } = data;
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;
}
}
}
</script>
<AppObjectCore
{...commonProps}
title={data.displayName || data.server}
icon="img server"
isBold={_.get($currentDatabase, 'connection._id') == data._id}
statusIcon={statusIcon || engineStatusIcon}
statusTitle={statusTitle || engineStatusTitle}
{extInfo}
on:click
on:click={() => ($openedConnections = _.uniq([...$openedConnections, data._id]))}
/>

View File

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

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import _ from 'lodash';
import { currentDatabase } from '../stores';
import AppObjectCore from './AppObjectCore.svelte';
export let data;
export let commonProps;
</script>
<AppObjectCore
{...commonProps}
{data}
title={data.name}
icon="img database"
isBold={_.get($currentDatabase, 'connection._id') == _.get(data.connection, '_id') &&
_.get($currentDatabase, 'name') == data.name}
on:click={() => ($currentDatabase = data)}
/>

View File

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

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import _ from 'lodash';
import AppObjectCore from './AppObjectCore.svelte';
import { currentDatabase, openedConnections } from '../stores';
import openNewTab from '../utility/openNewTab';
export let commonProps;
export let data;
const icons = {
tables: 'img table',
views: 'img view',
procedures: 'img procedure',
functions: 'img function',
};
function handleClick() {
const { schemaName, pureName, conid, database, objectTypeField } = data;
openNewTab({
title: data.pureName,
icon: 'img table',
tabComponent: 'TableDataTab',
props: {
schemaName,
pureName,
conid,
database,
objectTypeField,
},
});
}
</script>
<AppObjectCore
{...commonProps}
{data}
title={data.schemaName ? `${data.schemaName}.${data.pureName}` : data.pureName}
icon={icons[data.objectTypeField]}
on:click={handleClick}
/>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import { useDatabaseList } from '../utility/metadataLoaders';
import AppObjectList from './AppObjectList.svelte';
import DatabaseAppObject from './DatabaseAppObject.svelte';
export let data;
$: databases = useDatabaseList({ conid: data._id });
</script>
<AppObjectList list={($databases || []).map(db => ({ ...db, connection: data }))} component={DatabaseAppObject} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { commands } from '../stores';
import { get } from 'svelte/store';
function handleKeyDown(e) {
let keyText = '';
if (e.ctrlKey) keyText += 'Ctrl+';
if (e.shiftKey) keyText += 'Shift+';
if (e.altKey) keyText += 'Alt+';
keyText += e.key;
// console.log('keyText', keyText);
const commandsValue = get(commands);
const command: any = Object.values(commandsValue).find(
(x: any) => x.enabled && x.keyText && x.keyText.toLowerCase() == keyText.toLowerCase()
);
if (command) {
e.preventDefault();
command.onClick();
}
}
</script>
<svelte:window on:keydown={handleKeyDown} />

View File

@@ -0,0 +1,105 @@
<script context="module">
registerCommand({
id: 'commandPalette.show',
category: 'Command palette',
name: 'Show',
toolbarName: 'Menu',
toolbarOrder: 0,
keyText: 'F1',
toolbar: true,
showDisabled: true,
icon: 'icon menu',
onClick: () => visibleCommandPalette.set(true),
enabledStore: derived(visibleCommandPalette, $visibleCommandPalette => !$visibleCommandPalette),
});
</script>
<script>
import { filterName } from 'dbgate-datalib';
import _ from 'lodash';
import { derived } from 'svelte/store';
import { onMount } from 'svelte';
import { commands, visibleCommandPalette } from '../stores';
import { clickOutside } from '../utility/clickOutside';
import keycodes from '../utility/keycodes';
import registerCommand from './registerCommand';
let domInput;
let parentCommand;
let filter = '';
$: selectedIndex = true ? 0 : filter;
onMount(() => domInput.focus());
$: sortedComands = _.sortBy(
Object.values($commands).filter(x => x.enabled),
'text'
);
$: filteredItems = (parentCommand ? parentCommand.getSubCommands() : sortedComands).filter(x =>
filterName(filter, x.text)
);
function handleCommand(command) {
if (command.getSubCommands) {
parentCommand = command;
domInput.focus();
selectedIndex = 0;
} else {
$visibleCommandPalette = false;
command.onClick();
}
}
function handleKeyDown(e) {
if (e.keyCode == keycodes.upArrow && selectedIndex > 0) selectedIndex--;
if (e.keyCode == keycodes.downArrow && selectedIndex < filteredItems.length - 1) selectedIndex++;
if (e.keyCode == keycodes.enter) handleCommand(filteredItems[selectedIndex]);
if (e.keyCode == keycodes.escape) $visibleCommandPalette = false;
}
</script>
<div class="main" use:clickOutside on:clickOutside={() => ($visibleCommandPalette = false)}>
<div class="search">
<input type="text" bind:this={domInput} bind:value={filter} on:keydown={handleKeyDown} />
</div>
{#each filteredItems as command, index}
<div class="command" class:selected={index == selectedIndex} on:click={() => handleCommand(command)}>
<div>{command.text}</div>
{#if command.keyText}
<div class="shortcut">{command.keyText}</div>
{/if}
</div>
{/each}
</div>
<style>
.main {
width: 500px;
max-height: 500px;
background: var(--theme-bg-2);
padding: 5px;
}
.search {
display: flex;
}
input {
width: 100%;
}
.command {
padding: 5px;
display: flex;
justify-content: space-between;
}
.command:hover {
background: var(--theme-bg-3);
}
.command.selected {
background: var(--theme-bg-selected);
}
.shortcut {
background: var(--theme-bg-3);
}
</style>

View File

@@ -0,0 +1,46 @@
import { commands } from '../stores';
export interface SubCommand {
text: string;
onClick: Function;
}
export interface GlobalCommand {
id: string;
category: string;
name: string;
text: string /* category: name */;
keyText?: string;
getSubCommands?: () => SubCommand[];
onClick?: Function;
enabledStore?: any;
icon?: string;
toolbar?: boolean;
enabled?: boolean;
showDisabled?: boolean;
toolbarName?: string;
toolbarOrder?: number;
}
export default function registerCommand(command: GlobalCommand) {
const { enabledStore } = command;
commands.update(x => ({
...x,
[command.id]: {
text: `${command.category}: ${command.name}`,
...command,
enabled: !enabledStore,
},
}));
if (enabledStore) {
enabledStore.subscribe(value => {
commands.update(x => ({
...x,
[command.id]: {
...x[command.id],
enabled: value,
},
}));
});
}
}

View File

@@ -0,0 +1,9 @@
import { get } from 'svelte/store';
import { commands } from '../stores';
import { GlobalCommand } from './registerCommand';
export default function runCommand(commandId: string) {
const commandsValue = get(commands);
const command: GlobalCommand = commandsValue[commandId];
if (command.enabled) command.onClick();
}

View File

@@ -0,0 +1,41 @@
import { currentTheme, extensions, visibleToolbar } from '../stores';
import registerCommand from './registerCommand';
import { derived, get } from 'svelte/store';
import { ThemeDefinition } from 'dbgate-types';
function themeCommand(theme: ThemeDefinition) {
return {
text: theme.themeName,
onClick: () => currentTheme.set(theme.className),
// onPreview: () => {
// const old = get(currentTheme);
// currentTheme.set(css);
// return ok => {
// if (!ok) currentTheme.set(old);
// };
// },
};
}
registerCommand({
id: 'theme.changeTheme',
category: 'Theme',
name: 'Change',
getSubCommands: () => get(extensions).themes.map(themeCommand),
});
registerCommand({
id: 'toolbar.show',
category: 'Toolbar',
name: 'Show',
onClick: () => visibleToolbar.set(1),
enabledStore: derived(visibleToolbar, $visibleToolbar => !$visibleToolbar),
});
registerCommand({
id: 'toolbar.hide',
category: 'Toolbar',
name: 'Hide',
onClick: () => visibleToolbar.set(0),
enabledStore: derived(visibleToolbar, $visibleToolbar => $visibleToolbar),
});

View File

@@ -155,10 +155,10 @@ export default class ChangeSetGrider extends Grider {
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];
}
// static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
// return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
// }
// static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
// return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
// }
}

View File

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

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
import DropDownButton from '../widgets/DropDownButton.svelte';
import ColumnLabel from './ColumnLabel.svelte';
export let column;
export let conid = undefined;
export let database = undefined;
export let grouping = undefined;
export let order = undefined;
</script>
<div class="header">
<div class="label">
{#if grouping}
<span class="grouping">
{grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}
</span>
{/if}
<ColumnLabel {...column} />
</div>
{#if order == 'ASC'}
<span class="icon">
<FontIcon icon="img sort-asc" />
</span>
{/if}
{#if order == 'DESC'}
<span class="icon">
<FontIcon icon="img sort-desc" />
</span>
{/if}
<DropDownButton />
</div>
<style>
.header {
display: flex;
flex-wrap: nowrap;
}
.label {
flex: 1;
min-width: 10px;
padding: 2px;
margin: auto;
white-space: nowrap;
}
.icon {
margin-left: 3px;
}
.resizer {
background-color: var(--theme-border);
width: 2px;
cursor: col-resize;
z-index: 1;
}
.grouping {
color: var(--theme-font-alt);
white-space: nowrap;
}
</style>

View File

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

View File

@@ -0,0 +1,46 @@
<script context="module" lang="ts">
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;
}
</script>
<script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
export let notNull = false;
export let forceIcon = false;
export let headerText = '';
export let columnName = '';
export let extInfo = null;
$: icon = getColumnIcon($$props, forceIcon);
</script>
<span class="label" class:notNull>
{#if icon}
<FontIcon {icon} />
{/if}
{headerText || columnName}
{#if extInfo}
<span class="extinfo">{extInfo}</span>
{/if}
</span>
<style>
.label {
white-space: nowrap;
}
.label.notNull {
font-weight: bold;
}
.extinfo {
font-weight: normal;
margin-left: 5px;
color: var(--theme-font-3);
}
</style>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
<script lang="ts">
export let config;
export let gridCoreComponent;
</script>
<svelte:component this={gridCoreComponent} {...$$props} />

View File

@@ -0,0 +1,134 @@
<script context="module">
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?$/;
</script>
<script lang="ts">
import moment from 'moment';
import _ from 'lodash';
import { isTypeLogical } from 'dbgate-tools';
export let rowIndex;
export let col;
export let rowData;
export let hintFieldsAllowed = undefined;
export let isSelected = false;
export let isFrameSelected = false;
export let isModifiedRow = false;
export let isModifiedCell = false;
export let isInserted = false;
export let isDeleted = false;
export let isAutofillSelected = false;
$: value = (rowData || {})[col.uniqueName];
</script>
<td
data-row={rowIndex}
data-col={col.colIndex}
class:isSelected
class:isFrameSelected
class:isModifiedRow
class:isModifiedCell
class:isInserted
class:isDeleted
class:isAutofillSelected
style={`width:${col.width}px; min-width:${col.width}px; max-width:${col.width}px`}
>
{#if value == null}
<span class="null">(NULL)</span>
{:else if _.isDate(value)}
{moment(value).format('YYYY-MM-DD HH:mm:ss')}
{:else if value === true}
1
{:else if value === false}
0
{:else if _.isNumber(value)}
{#if value >= 10000 || value <= -10000}
{value.toLocaleString()}
{:else}
{value.toString()}
{/if}
{:else if _.isString(value)}
{#if dateTimeRegex.test(value)}
{moment(value).format('YYYY-MM-DD HH:mm:ss')}
{:else}
{highlightSpecialCharacters(value)}
{/if}
{:else if _.isPlainObject(value)}
{#if _.isArray(value.data)}
{#if value.data.length == 1 && isTypeLogical(col.dataType)}
{value.data[0]}
{:else}
<span class="null">({value.data.length} bytes)</span>
{/if}
{:else}
<span class="null">(RAW)</span>
{/if}
{:else}
{value.toString()}
{/if}
{#if hintFieldsAllowed && hintFieldsAllowed.includes(col.uniqueName) && rowData}
<span class="hint">{rowData[col.hintColumnName]}</span>
{/if}
</td>
<style>
td {
font-weight: normal;
border: 1px solid var(--theme-border);
padding: 2px;
white-space: nowrap;
position: relative;
overflow: hidden;
}
td.isSelected {
background: var(--theme-bg-selected);
}
td.isFrameSelected {
outline: 3px solid var(--theme-bg-selected);
outline-offset: -3px;
}
td.isAutofillSelected {
outline: 3px solid var(--theme-bg-selected);
outline-offset: -3px;
}
td.isModifiedRow {
background: var(--theme-bg-gold);
}
td.isModifiedCell {
background: var(--theme-bg-orange);
}
td.isInserted {
background: var(--theme-bg-green);
}
td.isDeleted {
background: var(--theme-bg-volcano);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
background-repeat: repeat-x;
background-position: 50% 50%;
}
.hint {
color: var(--theme-font-3);
margin-left: 5px;
}
.null {
color: var(--theme-font-3);
font-style: italic;
}
</style>

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,324 @@
<script lang="ts" context="module">
const currentDataGrid = writable(null);
registerCommand({
id: 'dataGrid.refresh',
category: 'Data grid',
name: 'Refresh',
keyText: 'F5',
toolbar: true,
icon: 'icon reload',
enabledStore: derived([currentDataGrid], ([grid]) => grid != null),
onClick: () => get(currentDataGrid).refresh(),
});
</script>
<script lang="ts">
import { GridDisplay } from 'dbgate-datalib';
import _ from 'lodash';
import { writable, get, derived } from 'svelte/store';
import registerCommand from '../commands/registerCommand';
import ColumnHeaderControl from './ColumnHeaderControl.svelte';
import DataGridRow from './DataGridRow.svelte';
import {
cellIsSelected,
countColumnSizes,
countVisibleRealColumns,
filterCellForRow,
filterCellsForRow,
} from './gridutil';
import HorizontalScrollBar from './HorizontalScrollBar.svelte';
import { cellFromEvent, emptyCellArray, getCellRange, isRegularCell, nullCell, topLeftCell } from './selection';
import VerticalScrollBar from './VerticalScrollBar.svelte';
export let loadNextData = undefined;
export let grider = undefined;
export let display: GridDisplay = undefined;
export let conid = undefined;
export let database = undefined;
export let frameSelection = undefined;
export let instance = undefined;
const wheelRowCount = 5;
let containerHeight = 0;
let containerWidth = 0;
let rowHeight = 0;
let firstVisibleRowScrollIndex = 0;
let firstVisibleColumnScrollIndex = 0;
let domFocusField;
let domHorizontalScroll;
let domVerticalScroll;
let currentCell = topLeftCell;
let selectedCells = [topLeftCell];
let dragStartCell = nullCell;
let shiftDragStartCell = nullCell;
let autofillDragStartCell = nullCell;
let autofillSelectedCells = emptyCellArray;
$: autofillMarkerCell =
selectedCells && selectedCells.length > 0 && _.uniq(selectedCells.map(x => x[0])).length == 1
? [_.max(selectedCells.map(x => x[0])), _.max(selectedCells.map(x => x[1]))]
: null;
// $: firstVisibleRowScrollIndex = 0;
// $: visibleRowCountUpperBound = 25;
// $: console.log('grider', grider);
$: columns = display.allColumns;
$: columnSizes = countColumnSizes(grider, columns, containerWidth, display);
$: headerColWidth = 40;
$: gridScrollAreaHeight = containerHeight - 2 * rowHeight;
$: gridScrollAreaWidth = containerWidth - columnSizes.frozenSize - headerColWidth - 32;
$: visibleRowCountUpperBound = Math.ceil(gridScrollAreaHeight / Math.floor(Math.max(1, rowHeight)));
$: visibleRowCountLowerBound = Math.floor(gridScrollAreaHeight / Math.ceil(Math.max(1, rowHeight)));
$: visibleRealColumns = countVisibleRealColumns(
columnSizes,
firstVisibleColumnScrollIndex,
gridScrollAreaWidth,
columns
);
// $: console.log('visibleRealColumns', visibleRealColumns);
// $: console.log('visibleRowCountUpperBound', visibleRowCountUpperBound);
// $: console.log('rowHeight', rowHeight);
// $: console.log('containerHeight', containerHeight);
$: realColumnUniqueNames = _.range(columnSizes.realCount).map(
realIndex => (columns[columnSizes.realToModel(realIndex)] || {}).uniqueName
);
$: maxScrollColumn = columnSizes.scrollInView(0, columns.length - 1 - columnSizes.frozenCount, gridScrollAreaWidth);
$: {
if (loadNextData && firstVisibleRowScrollIndex + visibleRowCountUpperBound >= grider.rowCount) {
loadNextData();
}
}
function handleGridMouseDown(event) {
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 (domFocusField) domFocusField.focus();
const cell = cellFromEvent(event);
if (event.button == 2 && cell && cellIsSelected(cell[0], cell[1], selectedCells)) return;
const autofill = event.target.closest('div.autofillHandleMarker');
if (autofill) {
autofillDragStartCell = cell;
} else {
currentCell = cell;
if (event.ctrlKey) {
if (isRegularCell(cell)) {
if (selectedCells.find(x => x[0] == cell[0] && x[1] == cell[1])) {
selectedCells = selectedCells.filter(x => x[0] != cell[0] || x[1] != cell[1]);
} else {
selectedCells = [...selectedCells, cell];
}
}
} else {
selectedCells = getCellRange(cell, cell);
dragStartCell = cell;
// if (isRegularCell(cell) && !_.isEqual(cell, inplaceEditorState.cell) && _.isEqual(cell, currentCell)) {
// // @ts-ignore
// dispatchInsplaceEditor({ type: 'show', cell, selectAll: true });
// } else if (!_.isEqual(cell, inplaceEditorState.cell)) {
// // @ts-ignore
// dispatchInsplaceEditor({ type: 'close' });
// }
}
}
if (display.focusedColumn) display.focusColumn(null);
}
function handleGridMouseMove(event) {
if (autofillDragStartCell) {
const cell = cellFromEvent(event);
if (isRegularCell(cell) && (cell[0] == autofillDragStartCell[0] || cell[1] == autofillDragStartCell[1])) {
const autoFillStart = [selectedCells[0][0], _.min(selectedCells.map(x => x[1]))];
// @ts-ignore
autofillSelectedCells = getCellRange(autoFillStart, cell);
}
} else if (dragStartCell) {
const cell = cellFromEvent(event);
currentCell = cell;
selectedCells = getCellRange(dragStartCell, cell);
}
}
function handleGridMouseUp(event) {
if (dragStartCell) {
const cell = cellFromEvent(event);
currentCell = cell;
selectedCells = getCellRange(dragStartCell, cell);
dragStartCell = null;
}
if (autofillDragStartCell) {
const currentRowNumber = currentCell[0];
if (_.isNumber(currentRowNumber)) {
const rowIndexes = _.uniq((autofillSelectedCells || []).map(x => x[0])).filter(x => x != currentRowNumber);
const colNames = selectedCells.map(cell => realColumnUniqueNames[cell[1]]);
const changeObject = _.pick(grider.getRowData(currentRowNumber), colNames);
grider.beginUpdate();
for (const index of rowIndexes) grider.updateRow(index, changeObject);
grider.endUpdate();
}
autofillDragStartCell = null;
autofillSelectedCells = [];
selectedCells = autofillSelectedCells;
}
}
function handleGridWheel(event) {
let newFirstVisibleRowScrollIndex = firstVisibleRowScrollIndex;
if (event.deltaY > 0) {
newFirstVisibleRowScrollIndex += wheelRowCount;
}
if (event.deltaY < 0) {
newFirstVisibleRowScrollIndex -= wheelRowCount;
}
let rowCount = grider.rowCount;
if (newFirstVisibleRowScrollIndex + visibleRowCountLowerBound > rowCount) {
newFirstVisibleRowScrollIndex = rowCount - visibleRowCountLowerBound + 1;
}
if (newFirstVisibleRowScrollIndex < 0) {
newFirstVisibleRowScrollIndex = 0;
}
firstVisibleRowScrollIndex = newFirstVisibleRowScrollIndex;
domVerticalScroll.scroll(newFirstVisibleRowScrollIndex);
}
export function refresh() {
display.reload();
}
</script>
<div class="container" bind:clientWidth={containerWidth} bind:clientHeight={containerHeight}>
<input
type="text"
class="focus-field"
bind:this={domFocusField}
on:focus={() => {
currentDataGrid.set(instance);
}}
/>
<table
class="table"
on:mousedown={handleGridMouseDown}
on:mousemove={handleGridMouseMove}
on:mouseup={handleGridMouseUp}
on:wheel={handleGridWheel}
>
<thead>
<tr>
<td
class="header-cell"
data-row="header"
data-col="header"
bind:clientHeight={rowHeight}
style={`width:${headerColWidth}px; min-width:${headerColWidth}px; max-width:${headerColWidth}px`}
/>
{#each visibleRealColumns as col (col.uniqueName)}
<td
class="header-cell"
data-row="header"
data-col={col.colIndex}
style={`width:${col.width}px; min-width:${col.width}px; max-width:${col.width}px`}
>
<ColumnHeaderControl column={col} {conid} {database} />
</td>
{/each}
</tr>
</thead>
<tbody>
{#each _.range(firstVisibleRowScrollIndex, firstVisibleRowScrollIndex + visibleRowCountUpperBound) as rowIndex (rowIndex)}
<DataGridRow
{rowIndex}
{grider}
{visibleRealColumns}
{rowHeight}
{autofillSelectedCells}
selectedCells={filterCellsForRow(selectedCells, rowIndex)}
autofillMarkerCell={filterCellForRow(autofillMarkerCell, rowIndex)}
{frameSelection}
/>
{/each}
</tbody>
</table>
<HorizontalScrollBar
minimum={0}
maximum={maxScrollColumn}
viewportRatio={gridScrollAreaWidth / columnSizes.getVisibleScrollSizeSum()}
on:scroll={e => (firstVisibleColumnScrollIndex = e.detail)}
bind:this={domHorizontalScroll}
/>
<VerticalScrollBar
minimum={0}
maximum={grider.rowCount - visibleRowCountUpperBound + 2}
viewportRatio={visibleRowCountUpperBound / grider.rowCount}
on:scroll={e => (firstVisibleRowScrollIndex = e.detail)}
bind:this={domVerticalScroll}
/>
</div>
<style>
.container {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
user-select: none;
}
.table {
position: absolute;
left: 0;
top: 0;
bottom: 20px;
overflow: scroll;
border-collapse: collapse;
outline: none;
}
.header-cell {
border: 1px solid var(--theme-border);
text-align: left;
padding: 0;
margin: 0;
background-color: var(--theme-bg-1);
overflow: hidden;
}
.filter-cell {
text-align: left;
overflow: hidden;
margin: 0;
padding: 0;
}
.focus-field {
position: absolute;
left: -1000px;
top: -1000px;
}
.row-count-label {
position: absolute;
background-color: var(--theme-bg-2);
right: 40px;
bottom: 20px;
}
</style>

View File

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

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import DataGridCell from './DataGridCell.svelte';
import { cellIsSelected } from './gridutil';
import RowHeaderCell from './RowHeaderCell.svelte';
export let rowHeight;
export let rowIndex;
export let visibleRealColumns: any[];
export let grider;
export let frameSelection = undefined;
export let selectedCells = undefined;
export let autofillSelectedCells = undefined;
export let autofillMarkerCell = undefined;
$: rowData = grider.getRowData(rowIndex);
$: rowStatus = grider.getRowStatus(rowIndex);
$: 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);
</script>
<tr style={`height: ${rowHeight}px`}>
<RowHeaderCell {rowIndex} />
{#each visibleRealColumns as col (col.uniqueName)}
<DataGridCell
{rowIndex}
{rowData}
{col}
{hintFieldsAllowed}
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
/>
{/each}
</tr>
<style>
tr {
background-color: var(--theme-bg-0);
}
tr:nth-child(6n + 3) {
background-color: var(--theme-bg-1);
}
tr:nth-child(6n + 6) {
background-color: var(--theme-bg-alt);
}
</style>

View File

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

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let viewportRatio = 0.5;
export let minimum;
export let maximum;
const dispatch = createEventDispatcher();
let width;
let node;
$: contentSize = Math.round(width / viewportRatio);
function handleScroll() {
const position = node.scrollLeft;
const ratio = position / (contentSize - width);
if (ratio < 0) return 0;
const res = ratio * (maximum - minimum + 1) + minimum;
dispatch('scroll', Math.floor(res + 0.3));
}
export function scroll(value) {
const position01 = (value - minimum) / (maximum - minimum + 1);
const position = position01 * (contentSize - width);
if (node) node.scrollLeft = Math.floor(position);
}
</script>
<div bind:clientWidth={width} bind:this={node} on:scroll={handleScroll} class="main">
<div style={`width: ${contentSize}px`}>&nbsp;</div>
</div>
<style>
.main {
overflow-x: scroll;
height: 16px;
position: absolute;
bottom: 0;
right: 0;
left: 0;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
<script lang="ts">
import DataGridCore from './DataGridCore.svelte';
export let loadDataPage;
export let dataPageAvailable;
export let loadRowCount;
export let grider;
export let display;
// export let griderFactory;
export let loadedRows = [];
let isLoading = false;
let isLoadedAll = false;
let loadedTime = new Date().getTime();
let allRowCount = null;
let errorMessage = null;
let loadNextDataToken = 0;
let domComponent;
async function loadNextData() {
if (isLoading) return;
isLoading = true;
const loadStart = new Date().getTime();
// loadedTimeRef.current = loadStart;
// console.log('LOAD NEXT ROWS', loadedRows);
const nextRows = await loadDataPage($$props, loadedRows.length, 100);
// if (loadedTimeRef.current !== loadStart) {
// // new load was dispatched
// return;
// }
isLoading = false;
if (nextRows.errorMessage) {
errorMessage = nextRows.errorMessage;
} else {
// if (allRowCount == null) handleLoadRowCount();
loadedRows = [...loadedRows, ...nextRows];
isLoadedAll = nextRows.length === 0;
// const loadedInfo = {
// loadedRows: [...loadedRows, ...nextRows],
// loadedTime,
// };
// setLoadProps(oldLoadProps => ({
// ...oldLoadProps,
// isLoading: false,
// isLoadedAll: oldLoadProps.loadNextDataToken == loadNextDataToken && nextRows.length === 0,
// loadNextDataToken,
// ...loadedInfo,
// }));
}
// console.log('LOADED', nextRows, loadedRows);
}
// $: griderProps = { ...$$props, sourceRows: loadProps.loadedRows };
// $: grider = griderFactory(griderProps);
function handleLoadNextData() {
if (!isLoadedAll && !errorMessage && !grider.disableLoadNextPage) {
if (dataPageAvailable($$props)) {
// If not, callbacks to load missing metadata are dispatched
loadNextData();
}
}
}
function reload() {
allRowCount = null;
isLoading = false;
loadedRows = [];
isLoadedAll = false;
loadedTime = new Date().getTime();
errorMessage = null;
loadNextDataToken = 0;
}
$: if (display.cache.refreshTime > loadedTime) {
reload();
}
</script>
<DataGridCore
{...$$props}
loadNextData={handleLoadNextData}
{grider}
bind:this={domComponent}
instance={domComponent}
/>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
<script lang="ts">
export let rowIndex;
</script>
<td data-row={rowIndex} data-col="header">
{rowIndex + 1}
</td>
<style>
td {
border: 1px solid var(--theme-border);
text-align: left;
padding: 2px;
background-color: var(--theme-bg-1);
overflow: hidden;
position: relative;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
<script context="module" lang="ts">
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);
}
</script>
<script lang="ts">
import axios from '../utility/axios';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
export let conid;
export let display;
export let database;
export let schemaName;
export let pureName;
export let config;
let loadedRows = [];
// $: console.log('loadedRows BIND', loadedRows);
$: grider = new ChangeSetGrider(loadedRows, null, null, display);
// $: console.log('GRIDER', grider);
</script>
<LoadingDataGridCore {...$$props} {loadDataPage} {dataPageAvailable} {loadRowCount} bind:loadedRows {grider} />

View File

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

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import { createGridCache, TableFormViewDisplay, TableGridDisplay } from 'dbgate-datalib';
import { findEngineDriver } from 'dbgate-tools';
import { writable } from 'svelte/store';
import { extensions } from '../stores';
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import DataGrid from './DataGrid.svelte';
import SqlDataGridCore from './SqlDataGridCore.svelte';
export let conid;
export let database;
export let schemaName;
export let pureName;
export let config;
$: connection = useConnectionInfo({ conid });
$: dbinfo = useDatabaseInfo({ conid, database });
const cache = writable(createGridCache());
// $: console.log('display', display);
$: display = connection
? new TableGridDisplay(
{ schemaName, pureName },
findEngineDriver($connection, $extensions),
$config,
config.update,
$cache,
cache.update,
$dbinfo
)
: // ? new TableFormViewDisplay(
// { schemaName, pureName },
// findEngineDriver(connection, $extensions),
// $config,
// config.update,
// $cache,
// cache.update,
// $dbinfo
// )
null;
</script>
<DataGrid {...$$props} gridCoreComponent={SqlDataGridCore} {display} />

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let viewportRatio = 0.5;
export let minimum;
export let maximum;
const dispatch = createEventDispatcher();
let height;
let node;
$: contentSize = Math.round(height / viewportRatio);
function handleScroll() {
const position = node.scrollTop;
const ratio = position / (contentSize - height);
if (ratio < 0) return 0;
let res = ratio * (maximum - minimum + 1) + minimum;
dispatch('scroll', Math.floor(res + 0.3));
}
export function scroll(value) {
const position01 = (value - minimum) / (maximum - minimum + 1);
const position = position01 * (contentSize - height);
if (node) node.scrollTop = Math.floor(position);
}
</script>
<div bind:clientHeight={height} bind:this={node} on:scroll={handleScroll} class="main">
<div style={`height: ${contentSize}px`}>&nbsp;</div>
</div>
<style>
.main {
overflow-y: scroll;
width: 20px;
position: absolute;
right: 0px;
width: 20px;
bottom: 16px;
top: 0;
}
</style>

View File

@@ -89,7 +89,7 @@ export function countVisibleRealColumns(columnSizes, firstVisibleColumnScrollInd
const visibleRealColumnIndexes = [];
const modelIndexes = {};
/** @type {(import('dbgate-datalib').DisplayColumn & {widthPx: string; colIndex: number})[]} */
/** @type {(import('dbgate-datalib').DisplayColumn & {width: number; colIndex: number})[]} */
const realColumns = [];
// frozen columns
@@ -112,12 +112,11 @@ export function countVisibleRealColumns(columnSizes, firstVisibleColumnScrollInd
let col = columns[modelColumnIndex];
if (!col) continue;
const widthNumber = columnSizes.getSizeByRealIndex(colIndex);
const width = columnSizes.getSizeByRealIndex(colIndex);
realColumns.push({
...col,
colIndex,
widthNumber,
widthPx: `${widthNumber}px`,
width,
});
}
return realColumns;

View File

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

View File

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

View File

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

View File

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

View File

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

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