mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-20 13:36:02 +00:00
@@ -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",
|
||||
|
||||
6
packages/types/extensions.d.ts
vendored
6
packages/types/extensions.d.ts
vendored
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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
105
packages/web/README.md
Normal 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
|
||||
```
|
||||
@@ -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
419
packages/web/public/bulma.css
vendored
Normal 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;
|
||||
}
|
||||
19
packages/web/public/dimensions.css
Normal file
19
packages/web/public/dimensions.css
Normal 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 |
BIN
packages/web/public/favicon.png
Normal file
BIN
packages/web/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
75
packages/web/public/global.css
Normal file
75
packages/web/public/global.css
Normal 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;
|
||||
} */
|
||||
23
packages/web/public/icon-colors.css
Normal file
23
packages/web/public/icon-colors.css
Normal 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);
|
||||
}
|
||||
@@ -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 |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "DbGate",
|
||||
"name": "DbGate database tool",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
@@ -1,16 +0,0 @@
|
||||
body {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
div {
|
||||
color: white;
|
||||
font-size: 25pt;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
margin-top: 40px;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="splash.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>Starting DbGate...</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="116" height="116" id="svg2">
|
||||
<defs id="defs4"/>
|
||||
<metadata id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<text x="10.710938" y="111.5" id="text2996" xml:space="preserve" style="font-size:144px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"><tspan x="10.710938" y="111.5" id="tspan2998" style="font-size:150px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold">?</tspan></text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
99
packages/web/rollup.config.js
Normal file
99
packages/web/rollup.config.js
Normal 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,
|
||||
},
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react';
|
||||
import './index.css';
|
||||
import Screen from './Screen';
|
||||
import {
|
||||
CurrentWidgetProvider,
|
||||
CurrentDatabaseProvider,
|
||||
OpenedTabsProvider,
|
||||
OpenedConnectionsProvider,
|
||||
LeftPanelWidthProvider,
|
||||
CurrentArchiveProvider,
|
||||
CurrentThemeProvider,
|
||||
} from './utility/globalState';
|
||||
import { SocketProvider } from './utility/SocketProvider';
|
||||
import ConnectionsPinger from './utility/ConnectionsPinger';
|
||||
import { ModalLayerProvider } from './modals/showModal';
|
||||
import UploadsProvider from './utility/UploadsProvider';
|
||||
import ThemeHelmet from './themes/ThemeHelmet';
|
||||
import PluginsProvider from './plugins/PluginsProvider';
|
||||
import { ExtensionsProvider } from './utility/useExtensions';
|
||||
import { MenuLayerProvider } from './modals/showMenu';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<CurrentWidgetProvider>
|
||||
<CurrentDatabaseProvider>
|
||||
<SocketProvider>
|
||||
<OpenedTabsProvider>
|
||||
<OpenedConnectionsProvider>
|
||||
<LeftPanelWidthProvider>
|
||||
<ConnectionsPinger>
|
||||
<PluginsProvider>
|
||||
<ExtensionsProvider>
|
||||
<CurrentArchiveProvider>
|
||||
<CurrentThemeProvider>
|
||||
<UploadsProvider>
|
||||
<ModalLayerProvider>
|
||||
<MenuLayerProvider>
|
||||
<ThemeHelmet />
|
||||
<Screen />
|
||||
</MenuLayerProvider>
|
||||
</ModalLayerProvider>
|
||||
</UploadsProvider>
|
||||
</CurrentThemeProvider>
|
||||
</CurrentArchiveProvider>
|
||||
</ExtensionsProvider>
|
||||
</PluginsProvider>
|
||||
</ConnectionsPinger>
|
||||
</LeftPanelWidthProvider>
|
||||
</OpenedConnectionsProvider>
|
||||
</OpenedTabsProvider>
|
||||
</SocketProvider>
|
||||
</CurrentDatabaseProvider>
|
||||
</CurrentWidgetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
10
packages/web/src/App.svelte
Normal file
10
packages/web/src/App.svelte
Normal 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 />
|
||||
@@ -1,11 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontIcon } from './icons';
|
||||
import useTheme from './theme/useTheme';
|
||||
import getElectron from './utility/getElectron';
|
||||
import useExtensions from './utility/useExtensions';
|
||||
|
||||
const TargetStyled = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: ${props => props.theme.main_background_blue[3]};
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
const InfoBox = styled.div``;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-size: 50px;
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const InfoWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const TitleWrapper = styled.div`
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
`;
|
||||
|
||||
export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
|
||||
const theme = useTheme();
|
||||
const { fileFormats } = useExtensions();
|
||||
const electron = getElectron();
|
||||
const fileTypeNames = fileFormats.filter(x => x.readerFunc).map(x => x.name);
|
||||
if (electron) fileTypeNames.push('SQL');
|
||||
return (
|
||||
!!isDragActive && (
|
||||
<TargetStyled theme={theme}>
|
||||
<InfoBox>
|
||||
<IconWrapper>
|
||||
<FontIcon icon="icon cloud-upload" />
|
||||
</IconWrapper>
|
||||
<TitleWrapper>Drop the files to upload to DbGate</TitleWrapper>
|
||||
<InfoWrapper>Supported file types: {fileTypeNames.join(', ')}</InfoWrapper>
|
||||
</InfoBox>
|
||||
<input {...inputProps} />
|
||||
</TargetStyled>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import React from 'react';
|
||||
import dimensions from './theme/dimensions';
|
||||
import styled from 'styled-components';
|
||||
import TabsPanel from './TabsPanel';
|
||||
import TabContent from './TabContent';
|
||||
import WidgetIconPanel from './widgets/WidgetIconPanel';
|
||||
import { useCurrentWidget, useLeftPanelWidth, useSetLeftPanelWidth } from './utility/globalState';
|
||||
import WidgetContainer from './widgets/WidgetContainer';
|
||||
import ToolBar from './widgets/Toolbar';
|
||||
import StatusBar from './widgets/StatusBar';
|
||||
import { useSplitterDrag, HorizontalSplitHandle } from './widgets/Splitter';
|
||||
import { ModalLayer } from './modals/showModal';
|
||||
import DragAndDropFileTarget from './DragAndDropFileTarget';
|
||||
import { useUploadsZone } from './utility/UploadsProvider';
|
||||
import useTheme from './theme/useTheme';
|
||||
import { MenuLayer } from './modals/showMenu';
|
||||
import ErrorBoundary, { ErrorBoundaryTest } from './utility/ErrorBoundary';
|
||||
|
||||
const BodyDiv = styled.div`
|
||||
position: fixed;
|
||||
top: ${dimensions.tabsPanel.height + dimensions.toolBar.height}px;
|
||||
left: ${props => props.contentLeft}px;
|
||||
bottom: ${dimensions.statusBar.height}px;
|
||||
right: 0;
|
||||
background-color: ${props => props.theme.content_background};
|
||||
`;
|
||||
|
||||
const ToolBarDiv = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: ${props => props.theme.toolbar_background};
|
||||
height: ${dimensions.toolBar.height}px;
|
||||
`;
|
||||
|
||||
const IconBar = styled.div`
|
||||
position: fixed;
|
||||
top: ${dimensions.toolBar.height}px;
|
||||
left: 0;
|
||||
bottom: ${dimensions.statusBar.height}px;
|
||||
width: ${dimensions.widgetMenu.iconSize}px;
|
||||
background-color: ${props => props.theme.widget_background};
|
||||
`;
|
||||
|
||||
const LeftPanel = styled.div`
|
||||
position: fixed;
|
||||
top: ${dimensions.toolBar.height}px;
|
||||
left: ${dimensions.widgetMenu.iconSize}px;
|
||||
bottom: ${dimensions.statusBar.height}px;
|
||||
background-color: ${props => props.theme.left_background};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const TabsPanelContainer = styled.div`
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: ${dimensions.toolBar.height}px;
|
||||
left: ${props => props.contentLeft}px;
|
||||
height: ${dimensions.tabsPanel.height}px;
|
||||
right: 0;
|
||||
background-color: ${props => props.theme.tabs_background2};
|
||||
border-top: 1px solid ${props => props.theme.border};
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 7px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StausBarContainer = styled.div`
|
||||
position: fixed;
|
||||
height: ${dimensions.statusBar.height}px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: ${props => props.theme.statusbar_background};
|
||||
`;
|
||||
|
||||
const ScreenHorizontalSplitHandle = styled(HorizontalSplitHandle)`
|
||||
position: absolute;
|
||||
top: ${dimensions.toolBar.height}px;
|
||||
bottom: ${dimensions.statusBar.height}px;
|
||||
`;
|
||||
|
||||
// const StyledRoot = styled.div`
|
||||
// // color: ${(props) => props.theme.fontColor};
|
||||
// `;
|
||||
|
||||
export default function Screen() {
|
||||
const theme = useTheme();
|
||||
const currentWidget = useCurrentWidget();
|
||||
const leftPanelWidth = useLeftPanelWidth();
|
||||
const setLeftPanelWidth = useSetLeftPanelWidth();
|
||||
const contentLeft = currentWidget
|
||||
? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness
|
||||
: dimensions.widgetMenu.iconSize;
|
||||
const toolbarPortalRef = React.useRef();
|
||||
const statusbarPortalRef = React.useRef();
|
||||
const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff));
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useUploadsZone();
|
||||
|
||||
return (
|
||||
<div {...getRootProps()}>
|
||||
<ErrorBoundary>
|
||||
<ToolBarDiv theme={theme}>
|
||||
<ToolBar toolbarPortalRef={toolbarPortalRef} />
|
||||
</ToolBarDiv>
|
||||
<IconBar theme={theme}>
|
||||
<WidgetIconPanel />
|
||||
</IconBar>
|
||||
{!!currentWidget && (
|
||||
<LeftPanel theme={theme}>
|
||||
<ErrorBoundary>
|
||||
<WidgetContainer />
|
||||
</ErrorBoundary>
|
||||
</LeftPanel>
|
||||
)}
|
||||
{!!currentWidget && (
|
||||
<ScreenHorizontalSplitHandle
|
||||
onMouseDown={onSplitDown}
|
||||
theme={theme}
|
||||
style={{ left: leftPanelWidth + dimensions.widgetMenu.iconSize }}
|
||||
/>
|
||||
)}
|
||||
<TabsPanelContainer contentLeft={contentLeft} theme={theme}>
|
||||
<TabsPanel></TabsPanel>
|
||||
</TabsPanelContainer>
|
||||
<BodyDiv contentLeft={contentLeft} theme={theme}>
|
||||
<TabContent toolbarPortalRef={toolbarPortalRef} statusbarPortalRef={statusbarPortalRef} />
|
||||
</BodyDiv>
|
||||
<StausBarContainer theme={theme}>
|
||||
<StatusBar statusbarPortalRef={statusbarPortalRef} />
|
||||
</StausBarContainer>
|
||||
<ModalLayer />
|
||||
<MenuLayer />
|
||||
|
||||
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
packages/web/src/Screen.svelte
Normal file
103
packages/web/src/Screen.svelte
Normal 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>
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
71
packages/web/src/TabContent.svelte
Normal file
71
packages/web/src/TabContent.svelte
Normal 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>
|
||||
@@ -1,300 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DropDownMenuItem, DropDownMenuDivider } from './modals/DropDownMenu';
|
||||
|
||||
import { useOpenedTabs, useSetOpenedTabs, useCurrentDatabase, useSetCurrentDatabase } from './utility/globalState';
|
||||
import { getConnectionInfo } from './utility/metadataLoaders';
|
||||
import { FontIcon } from './icons';
|
||||
import useTheme from './theme/useTheme';
|
||||
import usePropsCompare from './utility/usePropsCompare';
|
||||
import { useShowMenu } from './modals/showMenu';
|
||||
import { setSelectedTabFunc } from './utility/common';
|
||||
import getElectron from './utility/getElectron';
|
||||
|
||||
// const files = [
|
||||
// { name: 'app.js' },
|
||||
// { name: 'BranchCategory', type: 'table', selected: true },
|
||||
// { name: 'ApplicationList' },
|
||||
// ];
|
||||
|
||||
const DbGroupHandler = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-content: stretch;
|
||||
`;
|
||||
|
||||
const DbWrapperHandler = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
`;
|
||||
|
||||
const DbNameWrapper = styled.div`
|
||||
text-align: center;
|
||||
font-size: 8pt;
|
||||
border-bottom: 1px solid ${props => props.theme.border};
|
||||
border-right: 1px solid ${props => props.theme.border};
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 1px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
// height: 15px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${props => props.theme.tabs_background3};
|
||||
}
|
||||
background-color: ${props =>
|
||||
// @ts-ignore
|
||||
props.selected ? props.theme.tabs_background1 : 'inherit'};
|
||||
`;
|
||||
|
||||
// const DbNameWrapperInner = styled.div`
|
||||
// position: absolute;
|
||||
// white-space: nowrap;
|
||||
// `;
|
||||
|
||||
const FileTabItem = styled.div`
|
||||
border-right: 1px solid ${props => props.theme.border};
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
min-width: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
color: ${props => props.theme.tabs_font_hover};
|
||||
}
|
||||
background-color: ${props =>
|
||||
// @ts-ignore
|
||||
props.selected ? props.theme.tabs_background1 : 'inherit'};
|
||||
`;
|
||||
|
||||
const FileNameWrapper = styled.span`
|
||||
margin-left: 5px;
|
||||
`;
|
||||
|
||||
const CloseButton = styled(FontIcon)`
|
||||
margin-left: 5px;
|
||||
color: gray;
|
||||
&:hover {
|
||||
color: ${props => props.theme.tabs_font2};
|
||||
}
|
||||
`;
|
||||
|
||||
function TabContextMenu({ close, closeAll, closeOthers, closeWithSameDb, closeWithOtherDb, props }) {
|
||||
const { database } = props || {};
|
||||
const { conid } = props || {};
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={close}>Close</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={closeAll}>Close all</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={closeOthers}>Close others</DropDownMenuItem>
|
||||
{conid && database && (
|
||||
<DropDownMenuItem onClick={closeWithSameDb}>Close with same DB - {database}</DropDownMenuItem>
|
||||
)}
|
||||
{conid && database && (
|
||||
<DropDownMenuItem onClick={closeWithOtherDb}>Close with other DB than {database}</DropDownMenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getTabDbName(tab) {
|
||||
if (tab.props && tab.props.conid && tab.props.database) return tab.props.database;
|
||||
if (tab.props && tab.props.archiveFolder) return tab.props.archiveFolder;
|
||||
return '(no DB)';
|
||||
}
|
||||
|
||||
function getTabDbKey(tab) {
|
||||
if (tab.props && tab.props.conid && tab.props.database) return `database://${tab.props.database}-${tab.props.conid}`;
|
||||
if (tab.props && tab.props.archiveFolder) return `archive://${tab.props.archiveFolder}`;
|
||||
return '_no';
|
||||
}
|
||||
|
||||
function getDbIcon(key) {
|
||||
if (key.startsWith('database://')) return 'icon database';
|
||||
if (key.startsWith('archive://')) return 'icon archive';
|
||||
return 'icon file';
|
||||
}
|
||||
|
||||
function buildTooltip(tab) {
|
||||
let res = tab.tooltip;
|
||||
if (tab.props && tab.props.savedFilePath) {
|
||||
if (res) res += '\n';
|
||||
res += tab.props.savedFilePath;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export default function TabsPanel() {
|
||||
// const formatDbKey = (conid, database) => `${database}-${conid}`;
|
||||
const theme = useTheme();
|
||||
const showMenu = useShowMenu();
|
||||
|
||||
const tabs = useOpenedTabs();
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const currentDb = useCurrentDatabase();
|
||||
const setCurrentDb = useSetCurrentDatabase();
|
||||
|
||||
const { name, connection } = currentDb || {};
|
||||
const currentDbKey = name && connection ? `database://${name}-${connection._id}` : '_no';
|
||||
|
||||
const handleTabClick = (e, tabid) => {
|
||||
if (e.target.closest('.tabCloseButton')) {
|
||||
return;
|
||||
}
|
||||
setOpenedTabs(files => setSelectedTabFunc(files, tabid));
|
||||
};
|
||||
const closeTabFunc = closeCondition => tabid => {
|
||||
setOpenedTabs(files => {
|
||||
const active = files.find(x => x.tabid == tabid);
|
||||
if (!active) return files;
|
||||
|
||||
const newFiles = files.map(x => ({
|
||||
...x,
|
||||
closedTime: x.closedTime || (closeCondition(x, active) ? new Date().getTime() : undefined),
|
||||
}));
|
||||
|
||||
if (newFiles.find(x => x.selected && x.closedTime == null)) {
|
||||
return newFiles;
|
||||
}
|
||||
|
||||
const selectedIndex = _.findLastIndex(newFiles, x => x.closedTime == null);
|
||||
|
||||
return newFiles.map((x, index) => ({
|
||||
...x,
|
||||
selected: index == selectedIndex,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const closeTab = closeTabFunc((x, active) => x.tabid == active.tabid);
|
||||
const closeAll = () => {
|
||||
const closedTime = new Date().getTime();
|
||||
setOpenedTabs(tabs =>
|
||||
tabs.map(tab => ({
|
||||
...tab,
|
||||
closedTime: tab.closedTime || closedTime,
|
||||
selected: false,
|
||||
}))
|
||||
);
|
||||
};
|
||||
const closeWithSameDb = closeTabFunc(
|
||||
(x, active) =>
|
||||
_.get(x, 'props.conid') == _.get(active, 'props.conid') &&
|
||||
_.get(x, 'props.database') == _.get(active, 'props.database')
|
||||
);
|
||||
const closeWithOtherDb = closeTabFunc(
|
||||
(x, active) =>
|
||||
_.get(x, 'props.conid') != _.get(active, 'props.conid') ||
|
||||
_.get(x, 'props.database') != _.get(active, 'props.database')
|
||||
);
|
||||
const closeOthers = closeTabFunc((x, active) => x.tabid != active.tabid);
|
||||
const handleMouseUp = (e, tabid) => {
|
||||
if (e.button == 1) {
|
||||
e.preventDefault();
|
||||
closeTab(tabid);
|
||||
}
|
||||
};
|
||||
const handleContextMenu = (event, tabid, props) => {
|
||||
event.preventDefault();
|
||||
showMenu(
|
||||
event.pageX,
|
||||
event.pageY,
|
||||
<TabContextMenu
|
||||
close={() => closeTab(tabid)}
|
||||
closeAll={closeAll}
|
||||
closeOthers={() => closeOthers(tabid)}
|
||||
closeWithSameDb={() => closeWithSameDb(tabid)}
|
||||
closeWithOtherDb={() => closeWithOtherDb(tabid)}
|
||||
props={props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const electron = getElectron();
|
||||
if (electron) {
|
||||
const { ipcRenderer } = electron;
|
||||
const activeTab = tabs.find(x => x.selected);
|
||||
window['dbgate_activeTabId'] = activeTab ? activeTab.tabid : null;
|
||||
ipcRenderer.send('update-menu');
|
||||
}
|
||||
}, [tabs]);
|
||||
|
||||
// console.log(
|
||||
// 't',
|
||||
// tabs.map(x => x.tooltip)
|
||||
// );
|
||||
const tabsWithDb = tabs
|
||||
.filter(x => !x.closedTime)
|
||||
.map(tab => ({
|
||||
...tab,
|
||||
tabDbName: getTabDbName(tab),
|
||||
tabDbKey: getTabDbKey(tab),
|
||||
}));
|
||||
const tabsByDb = _.groupBy(tabsWithDb, 'tabDbKey');
|
||||
const dbKeys = _.keys(tabsByDb).sort();
|
||||
|
||||
const handleSetDb = async props => {
|
||||
const { conid, database } = props || {};
|
||||
if (conid) {
|
||||
const connection = await getConnectionInfo({ conid, database });
|
||||
if (connection) {
|
||||
setCurrentDb({ connection, name: database });
|
||||
return;
|
||||
}
|
||||
}
|
||||
setCurrentDb(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{dbKeys.map(dbKey => (
|
||||
<DbWrapperHandler key={dbKey}>
|
||||
<DbNameWrapper
|
||||
// @ts-ignore
|
||||
selected={tabsByDb[dbKey][0].tabDbKey == currentDbKey}
|
||||
onClick={() => handleSetDb(tabsByDb[dbKey][0].props)}
|
||||
theme={theme}
|
||||
>
|
||||
<FontIcon icon={getDbIcon(dbKey)} /> {tabsByDb[dbKey][0].tabDbName}
|
||||
</DbNameWrapper>
|
||||
<DbGroupHandler>
|
||||
{_.sortBy(tabsByDb[dbKey], ['title', 'tabid']).map(tab => (
|
||||
<FileTabItem
|
||||
{...tab}
|
||||
title={buildTooltip(tab)}
|
||||
key={tab.tabid}
|
||||
theme={theme}
|
||||
onClick={e => handleTabClick(e, tab.tabid)}
|
||||
onMouseUp={e => handleMouseUp(e, tab.tabid)}
|
||||
onContextMenu={e => handleContextMenu(e, tab.tabid, tab.props)}
|
||||
>
|
||||
{<FontIcon icon={tab.busy ? 'icon loading' : tab.icon} />}
|
||||
<FileNameWrapper>{tab.title}</FileNameWrapper>
|
||||
<CloseButton
|
||||
icon="icon close"
|
||||
className="tabCloseButton"
|
||||
theme={theme}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
closeTab(tab.tabid);
|
||||
}}
|
||||
/>
|
||||
</FileTabItem>
|
||||
))}
|
||||
</DbGroupHandler>
|
||||
</DbWrapperHandler>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontIcon } from '../icons';
|
||||
import { useShowMenu } from '../modals/showMenu';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const AppObjectDiv = styled.div`
|
||||
padding: 5px;
|
||||
${props =>
|
||||
!props.disableHover &&
|
||||
`
|
||||
&:hover {
|
||||
background-color: ${props.theme.left_background_blue[1]};
|
||||
}
|
||||
`}
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-weight: ${props => (props.isBold ? 'bold' : 'normal')};
|
||||
`;
|
||||
|
||||
const IconWrap = styled.span`
|
||||
margin-right: 5px;
|
||||
`;
|
||||
|
||||
const StatusIconWrap = styled.span`
|
||||
margin-left: 5px;
|
||||
`;
|
||||
|
||||
const ExtInfoWrap = styled.span`
|
||||
font-weight: normal;
|
||||
margin-left: 5px;
|
||||
color: ${props => props.theme.left_font3};
|
||||
`;
|
||||
|
||||
export function AppObjectCore({
|
||||
title,
|
||||
icon,
|
||||
data,
|
||||
onClick = undefined,
|
||||
onClick2 = undefined,
|
||||
onClick3 = undefined,
|
||||
isBold = undefined,
|
||||
isBusy = undefined,
|
||||
prefix = undefined,
|
||||
statusIcon = undefined,
|
||||
extInfo = undefined,
|
||||
statusTitle = undefined,
|
||||
disableHover = false,
|
||||
children = null,
|
||||
Menu = undefined,
|
||||
...other
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const showMenu = useShowMenu();
|
||||
|
||||
const handleContextMenu = event => {
|
||||
if (!Menu) return;
|
||||
|
||||
event.preventDefault();
|
||||
showMenu(event.pageX, event.pageY, <Menu data={data} />);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppObjectDiv
|
||||
onContextMenu={handleContextMenu}
|
||||
onClick={() => {
|
||||
if (onClick) onClick(data);
|
||||
if (onClick2) onClick2(data);
|
||||
if (onClick3) onClick3(data);
|
||||
}}
|
||||
theme={theme}
|
||||
isBold={isBold}
|
||||
draggable
|
||||
onDragStart={e => {
|
||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
||||
}}
|
||||
disableHover={disableHover}
|
||||
{...other}
|
||||
>
|
||||
{prefix}
|
||||
<IconWrap>{isBusy ? <FontIcon icon="icon loading" /> : <FontIcon icon={icon} />}</IconWrap>
|
||||
{title}
|
||||
{statusIcon && (
|
||||
<StatusIconWrap>
|
||||
<FontIcon icon={statusIcon} title={statusTitle} />
|
||||
</StatusIconWrap>
|
||||
)}
|
||||
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
|
||||
</AppObjectDiv>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
57
packages/web/src/appobj/AppObjectCore.svelte
Normal file
57
packages/web/src/appobj/AppObjectCore.svelte
Normal 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>
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
14
packages/web/src/appobj/AppObjectList.svelte
Normal file
14
packages/web/src/appobj/AppObjectList.svelte
Normal 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}
|
||||
20
packages/web/src/appobj/AppObjectListItem.svelte
Normal file
20
packages/web/src/appobj/AppObjectListItem.svelte
Normal 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}
|
||||
@@ -1,75 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import axios from '../utility/axios';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
|
||||
function openArchive(openNewTab, fileName, folderName) {
|
||||
openNewTab({
|
||||
title: fileName,
|
||||
icon: 'img archive',
|
||||
tooltip: `${folderName}\n${fileName}`,
|
||||
tabComponent: 'ArchiveFileTab',
|
||||
props: {
|
||||
archiveFile: fileName,
|
||||
archiveFolder: folderName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function Menu({ data }) {
|
||||
const openNewTab = useOpenNewTab();
|
||||
const handleDelete = () => {
|
||||
axios.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
|
||||
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
|
||||
};
|
||||
const handleOpenRead = () => {
|
||||
openArchive(openNewTab, data.fileName, data.folderName);
|
||||
};
|
||||
const handleOpenWrite = async () => {
|
||||
// const resp = await axios.post('archive/load-free-table', { file: data.fileName, folder: data.folderName });
|
||||
|
||||
openNewTab({
|
||||
title: data.fileName,
|
||||
icon: 'img archive',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {
|
||||
initialArgs: {
|
||||
functionName: 'archiveReader',
|
||||
props: {
|
||||
fileName: data.fileName,
|
||||
folderName: data.folderName,
|
||||
},
|
||||
},
|
||||
archiveFile: data.fileName,
|
||||
archiveFolder: data.folderName,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={handleOpenRead}>Open (readonly)</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={handleOpenWrite}>Open in free table editor</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ArchiveFileAppObject({ data, commonProps }) {
|
||||
const { fileName, folderName } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
const onClick = () => {
|
||||
openArchive(openNewTab, fileName, folderName);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppObjectCore {...commonProps} data={data} title={fileName} icon="img archive" onClick={onClick} Menu={Menu} />
|
||||
);
|
||||
}
|
||||
|
||||
ArchiveFileAppObject.extractKey = data => data.fileName;
|
||||
ArchiveFileAppObject.createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
|
||||
|
||||
export default ArchiveFileAppObject;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import axios from '../utility/axios';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import { useCurrentArchive } from '../utility/globalState';
|
||||
|
||||
function Menu({ data }) {
|
||||
const handleDelete = () => {
|
||||
axios.post('archive/delete-folder', { folder: data.name });
|
||||
};
|
||||
return <>{data.name != 'default' && <DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>}</>;
|
||||
}
|
||||
|
||||
function ArchiveFolderAppObject({ data, commonProps }) {
|
||||
const { name } = data;
|
||||
const currentArchive = useCurrentArchive();
|
||||
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
data={data}
|
||||
title={name}
|
||||
icon="img archive-folder"
|
||||
isBold={name == currentArchive}
|
||||
Menu={Menu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ArchiveFolderAppObject.extractKey = data => data.name;
|
||||
ArchiveFolderAppObject.createMatcher = data => filter => filterName(filter, data.name);
|
||||
|
||||
export default ArchiveFolderAppObject;
|
||||
@@ -1,73 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import { setSelectedTabFunc } from '../utility/common';
|
||||
import styled from 'styled-components';
|
||||
import { FontIcon } from '../icons';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const InfoDiv = styled.div`
|
||||
margin-left: 30px;
|
||||
color: ${props => props.theme.left_font3};
|
||||
`;
|
||||
|
||||
function Menu({ data }) {
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const handleDelete = () => {
|
||||
setOpenedTabs(tabs => tabs.filter(x => x.tabid != data.tabid));
|
||||
};
|
||||
const handleDeleteOlder = () => {
|
||||
setOpenedTabs(tabs => tabs.filter(x => !x.closedTime || x.closedTime >= data.closedTime));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={handleDeleteOlder}>Delete older</DropDownMenuItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ClosedTabAppObject({ data, commonProps }) {
|
||||
const { tabid, props, selected, icon, title, closedTime, busy } = data;
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const theme = useTheme();
|
||||
|
||||
const onClick = () => {
|
||||
setOpenedTabs(files =>
|
||||
setSelectedTabFunc(
|
||||
files.map(x => ({
|
||||
...x,
|
||||
closedTime: x.tabid == tabid ? undefined : x.closedTime,
|
||||
})),
|
||||
tabid
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
data={data}
|
||||
title={`${title} ${moment(closedTime).fromNow()}`}
|
||||
icon={icon}
|
||||
isBold={!!selected}
|
||||
onClick={onClick}
|
||||
isBusy={busy}
|
||||
Menu={Menu}
|
||||
>
|
||||
{data.props && data.props.database && (
|
||||
<InfoDiv theme={theme}>
|
||||
<FontIcon icon="icon database" /> {data.props.database}
|
||||
</InfoDiv>
|
||||
)}
|
||||
{data.contentPreview && <InfoDiv theme={theme}>{data.contentPreview}</InfoDiv>}
|
||||
</AppObjectCore>
|
||||
);
|
||||
}
|
||||
|
||||
ClosedTabAppObject.extractKey = data => data.tabid;
|
||||
|
||||
export default ClosedTabAppObject;
|
||||
@@ -1,119 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import ConnectionModal from '../modals/ConnectionModal';
|
||||
import axios from '../utility/axios';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import ConfirmModal from '../modals/ConfirmModal';
|
||||
import CreateDatabaseModal from '../modals/CreateDatabaseModal';
|
||||
import { useCurrentDatabase, useOpenedConnections, useSetOpenedConnections } from '../utility/globalState';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import { useConfig } from '../utility/metadataLoaders';
|
||||
import useExtensions from '../utility/useExtensions';
|
||||
|
||||
function Menu({ data }) {
|
||||
const openedConnections = useOpenedConnections();
|
||||
const setOpenedConnections = useSetOpenedConnections();
|
||||
const showModal = useShowModal();
|
||||
const config = useConfig();
|
||||
|
||||
const handleEdit = () => {
|
||||
showModal(modalState => <ConnectionModal modalState={modalState} connection={data} />);
|
||||
};
|
||||
const handleDelete = () => {
|
||||
showModal(modalState => (
|
||||
<ConfirmModal
|
||||
modalState={modalState}
|
||||
message={`Really delete connection ${data.displayName || data.server}?`}
|
||||
onConfirm={() => axios.post('connections/delete', data)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
const handleCreateDatabase = () => {
|
||||
showModal(modalState => <CreateDatabaseModal modalState={modalState} conid={data._id} />);
|
||||
};
|
||||
const handleRefresh = () => {
|
||||
axios.post('server-connections/refresh', { conid: data._id });
|
||||
};
|
||||
const handleDisconnect = () => {
|
||||
setOpenedConnections(list => list.filter(x => x != data._id));
|
||||
};
|
||||
const handleConnect = () => {
|
||||
setOpenedConnections(list => _.uniq([...list, data._id]));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{config.runAsPortal == false && (
|
||||
<>
|
||||
<DropDownMenuItem onClick={handleEdit}>Edit</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{!openedConnections.includes(data._id) && <DropDownMenuItem onClick={handleConnect}>Connect</DropDownMenuItem>}
|
||||
{openedConnections.includes(data._id) && data.status && (
|
||||
<DropDownMenuItem onClick={handleRefresh}>Refresh</DropDownMenuItem>
|
||||
)}
|
||||
{openedConnections.includes(data._id) && (
|
||||
<DropDownMenuItem onClick={handleDisconnect}>Disconnect</DropDownMenuItem>
|
||||
)}
|
||||
{openedConnections.includes(data._id) && (
|
||||
<DropDownMenuItem onClick={handleCreateDatabase}>Create database</DropDownMenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectionAppObject({ data, commonProps }) {
|
||||
const { _id, server, displayName, engine, status } = data;
|
||||
const openedConnections = useOpenedConnections();
|
||||
const setOpenedConnections = useSetOpenedConnections();
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
const extensions = useExtensions();
|
||||
|
||||
const isBold = _.get(currentDatabase, 'connection._id') == _id;
|
||||
const onClick = () => setOpenedConnections(c => _.uniq([...c, _id]));
|
||||
|
||||
let statusIcon = null;
|
||||
let statusTitle = null;
|
||||
|
||||
let extInfo = null;
|
||||
if (extensions.drivers.find(x => x.engine == engine)) {
|
||||
const match = (engine || '').match(/^([^@]*)@/);
|
||||
extInfo = match ? match[1] : engine;
|
||||
} else {
|
||||
extInfo = engine;
|
||||
statusIcon = 'img warn';
|
||||
statusTitle = `Engine driver ${engine} not found, review installed plugins and change engine in edit connection dialog`;
|
||||
}
|
||||
|
||||
if (openedConnections.includes(_id)) {
|
||||
if (!status) statusIcon = 'icon loading';
|
||||
else if (status.name == 'pending') statusIcon = 'icon loading';
|
||||
else if (status.name == 'ok') statusIcon = 'img ok';
|
||||
else statusIcon = 'img error';
|
||||
if (status && status.name == 'error') {
|
||||
statusTitle = status.message;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
title={displayName || server}
|
||||
icon="img server"
|
||||
data={data}
|
||||
statusIcon={statusIcon}
|
||||
statusTitle={statusTitle}
|
||||
extInfo={extInfo}
|
||||
isBold={isBold}
|
||||
onClick={onClick}
|
||||
Menu={Menu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ConnectionAppObject.extractKey = data => data._id;
|
||||
ConnectionAppObject.createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server);
|
||||
|
||||
export default ConnectionAppObject;
|
||||
52
packages/web/src/appobj/ConnectionAppObject.svelte
Normal file
52
packages/web/src/appobj/ConnectionAppObject.svelte
Normal 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]))}
|
||||
/>
|
||||
@@ -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;
|
||||
20
packages/web/src/appobj/DatabaseAppObject.svelte
Normal file
20
packages/web/src/appobj/DatabaseAppObject.svelte
Normal 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)}
|
||||
/>
|
||||
@@ -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;
|
||||
40
packages/web/src/appobj/DatabaseObjectAppObject.svelte
Normal file
40
packages/web/src/appobj/DatabaseObjectAppObject.svelte
Normal 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}
|
||||
/>
|
||||
@@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import FavoriteModal from '../modals/FavoriteModal';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import axios from '../utility/axios';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
import { SavedFileAppObjectBase } from './SavedFileAppObject';
|
||||
|
||||
export function useOpenFavorite() {
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const openFavorite = React.useCallback(
|
||||
async favorite => {
|
||||
const { icon, tabComponent, title, props, tabdata } = favorite;
|
||||
let tabdataNew = tabdata;
|
||||
if (props.savedFile) {
|
||||
const resp = await axios.post('files/load', {
|
||||
folder: props.savedFolder,
|
||||
file: props.savedFile,
|
||||
format: props.savedFormat,
|
||||
});
|
||||
tabdataNew = {
|
||||
...tabdata,
|
||||
editor: resp.data,
|
||||
};
|
||||
}
|
||||
openNewTab(
|
||||
{
|
||||
title,
|
||||
icon: icon || 'img favorite',
|
||||
props,
|
||||
tabComponent,
|
||||
},
|
||||
tabdataNew
|
||||
);
|
||||
},
|
||||
[openNewTab]
|
||||
);
|
||||
|
||||
return openFavorite;
|
||||
}
|
||||
|
||||
export function FavoriteFileAppObject({ data, commonProps }) {
|
||||
const { icon, tabComponent, title, props, tabdata, urlPath } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
const showModal = useShowModal();
|
||||
const openFavorite = useOpenFavorite();
|
||||
const electron = getElectron();
|
||||
|
||||
const editFavorite = () => {
|
||||
showModal(modalState => <FavoriteModal modalState={modalState} editingData={data} />);
|
||||
};
|
||||
|
||||
const editFavoriteJson = async () => {
|
||||
const resp = await axios.post('files/load', {
|
||||
folder: 'favorites',
|
||||
file: data.file,
|
||||
format: 'text',
|
||||
});
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
icon: 'icon favorite',
|
||||
title,
|
||||
tabComponent: 'FavoriteEditorTab',
|
||||
props: {
|
||||
savedFile: data.file,
|
||||
savedFormat: 'text',
|
||||
savedFolder: 'favorites',
|
||||
},
|
||||
},
|
||||
{ editor: JSON.stringify(JSON.parse(resp.data), null, 2) }
|
||||
);
|
||||
};
|
||||
|
||||
const copyLink = () => {
|
||||
copyTextToClipboard(`${document.location.origin}#favorite=${urlPath}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="json"
|
||||
icon={icon || 'img favorite'}
|
||||
title={title}
|
||||
disableRename
|
||||
onLoad={async data => {
|
||||
openFavorite(data);
|
||||
}}
|
||||
menuExt={
|
||||
<>
|
||||
<DropDownMenuItem onClick={editFavorite}>Edit</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={editFavoriteJson}>Edit JSON definition</DropDownMenuItem>
|
||||
{!electron && urlPath && <DropDownMenuItem onClick={copyLink}>Copy link</DropDownMenuItem>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
FavoriteFileAppObject.extractKey = data => data.file;
|
||||
@@ -1,15 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
|
||||
function MacroAppObject({ data, commonProps }) {
|
||||
const { name, type, title, group } = data;
|
||||
|
||||
return <AppObjectCore {...commonProps} data={data} title={title} icon={'img macro'} />;
|
||||
}
|
||||
|
||||
MacroAppObject.extractKey = data => data.name;
|
||||
MacroAppObject.createMatcher = ({ name, title }) => filter => filterName(filter, name, title);
|
||||
|
||||
export default MacroAppObject;
|
||||
@@ -1,314 +0,0 @@
|
||||
import React from 'react';
|
||||
import axios from '../utility/axios';
|
||||
import _ from 'lodash';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import useNewQuery from '../query/useNewQuery';
|
||||
import { useCurrentDatabase } from '../utility/globalState';
|
||||
import ScriptWriter from '../impexp/ScriptWriter';
|
||||
import { extractPackageName } from 'dbgate-tools';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import InputTextModal from '../modals/InputTextModal';
|
||||
import useHasPermission from '../utility/useHasPermission';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
import ConfirmModal from '../modals/ConfirmModal';
|
||||
|
||||
function Menu({ data, menuExt = null, title = undefined, disableRename = false }) {
|
||||
const hasPermission = useHasPermission();
|
||||
const showModal = useShowModal();
|
||||
const handleDelete = () => {
|
||||
showModal(modalState => (
|
||||
<ConfirmModal
|
||||
modalState={modalState}
|
||||
message={`Really delete file ${title || data.file}?`}
|
||||
onConfirm={() => {
|
||||
axios.post('files/delete', data);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
const handleRename = () => {
|
||||
showModal(modalState => (
|
||||
<InputTextModal
|
||||
modalState={modalState}
|
||||
value={data.file}
|
||||
label="New file name"
|
||||
header="Rename file"
|
||||
onConfirm={newFile => {
|
||||
axios.post('files/rename', { ...data, newFile });
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{hasPermission(`files/${data.folder}/write`) && (
|
||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
||||
)}
|
||||
{hasPermission(`files/${data.folder}/write`) && !disableRename && (
|
||||
<DropDownMenuItem onClick={handleRename}>Rename</DropDownMenuItem>
|
||||
)}
|
||||
{menuExt}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedFileAppObjectBase({
|
||||
data,
|
||||
commonProps,
|
||||
format,
|
||||
icon,
|
||||
onLoad,
|
||||
title = undefined,
|
||||
menuExt = null,
|
||||
disableRename = false,
|
||||
}) {
|
||||
const { file, folder } = data;
|
||||
|
||||
const onClick = async () => {
|
||||
const resp = await axios.post('files/load', { folder, file, format });
|
||||
onLoad(resp.data);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
data={data}
|
||||
title={title || file}
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
Menu={props => <Menu {...props} menuExt={menuExt} title={title} disableRename={disableRename} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedSqlFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const newQuery = useNewQuery();
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const connection = _.get(currentDatabase, 'connection');
|
||||
const database = _.get(currentDatabase, 'name');
|
||||
|
||||
const handleGenerateExecute = () => {
|
||||
const script = new ScriptWriter();
|
||||
const conn = {
|
||||
..._.omit(connection, ['displayName', '_id']),
|
||||
database,
|
||||
};
|
||||
script.put(`const sql = await dbgateApi.loadFile('${folder}/${file}');`);
|
||||
script.put(`await dbgateApi.executeQuery({ sql, connection: ${JSON.stringify(conn)} });`);
|
||||
// @ts-ignore
|
||||
script.requirePackage(extractPackageName(conn.engine));
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Shell #',
|
||||
icon: 'img shell',
|
||||
tabComponent: 'ShellTab',
|
||||
},
|
||||
{ editor: script.getScript() }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="text"
|
||||
icon="img sql-file"
|
||||
menuExt={
|
||||
connection && database ? (
|
||||
<DropDownMenuItem onClick={handleGenerateExecute}>Generate shell execute</DropDownMenuItem>
|
||||
) : null
|
||||
}
|
||||
onLoad={data => {
|
||||
newQuery({
|
||||
title: file,
|
||||
initialData: data,
|
||||
// @ts-ignore
|
||||
savedFile: file,
|
||||
savedFolder: 'sql',
|
||||
savedFormat: 'text',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedShellFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="text"
|
||||
icon="img shell"
|
||||
onLoad={data => {
|
||||
openNewTab(
|
||||
{
|
||||
title: file,
|
||||
icon: 'img shell',
|
||||
tabComponent: 'ShellTab',
|
||||
props: {
|
||||
savedFile: file,
|
||||
savedFolder: 'shell',
|
||||
savedFormat: 'text',
|
||||
},
|
||||
},
|
||||
{ editor: data }
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedChartFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
|
||||
const connection = _.get(currentDatabase, 'connection') || {};
|
||||
const database = _.get(currentDatabase, 'name');
|
||||
|
||||
const tooltip = `${connection.displayName || connection.server}\n${database}`;
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="json"
|
||||
icon="img chart"
|
||||
onLoad={data => {
|
||||
openNewTab(
|
||||
{
|
||||
title: file,
|
||||
icon: 'img chart',
|
||||
tooltip,
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database,
|
||||
savedFile: file,
|
||||
savedFolder: 'charts',
|
||||
savedFormat: 'json',
|
||||
},
|
||||
tabComponent: 'ChartTab',
|
||||
},
|
||||
{ editor: data }
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedQueryFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
|
||||
const connection = _.get(currentDatabase, 'connection') || {};
|
||||
const database = _.get(currentDatabase, 'name');
|
||||
|
||||
const tooltip = `${connection.displayName || connection.server}\n${database}`;
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="json"
|
||||
icon="img query-design"
|
||||
onLoad={data => {
|
||||
openNewTab(
|
||||
{
|
||||
title: file,
|
||||
icon: 'img query-design',
|
||||
tooltip,
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database,
|
||||
savedFile: file,
|
||||
savedFolder: 'query',
|
||||
savedFormat: 'json',
|
||||
},
|
||||
tabComponent: 'QueryDesignTab',
|
||||
},
|
||||
{ editor: data }
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedMarkdownFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const showPage = () => {
|
||||
openNewTab({
|
||||
title: file,
|
||||
icon: 'img markdown',
|
||||
tabComponent: 'MarkdownViewTab',
|
||||
props: {
|
||||
savedFile: file,
|
||||
savedFolder: 'markdown',
|
||||
savedFormat: 'text',
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="text"
|
||||
icon="img markdown"
|
||||
onLoad={data => {
|
||||
openNewTab(
|
||||
{
|
||||
title: file,
|
||||
icon: 'img markdown',
|
||||
tabComponent: 'MarkdownEditorTab',
|
||||
props: {
|
||||
savedFile: file,
|
||||
savedFolder: 'markdown',
|
||||
savedFormat: 'text',
|
||||
},
|
||||
},
|
||||
{ editor: data }
|
||||
);
|
||||
}}
|
||||
menuExt={<DropDownMenuItem onClick={showPage}>Show page</DropDownMenuItem>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedFileAppObject({ data, commonProps }) {
|
||||
const { folder } = data;
|
||||
const folderTypes = {
|
||||
sql: SavedSqlFileAppObject,
|
||||
shell: SavedShellFileAppObject,
|
||||
charts: SavedChartFileAppObject,
|
||||
markdown: SavedMarkdownFileAppObject,
|
||||
query: SavedQueryFileAppObject,
|
||||
};
|
||||
const AppObject = folderTypes[folder];
|
||||
if (AppObject) {
|
||||
return <AppObject data={data} commonProps={commonProps} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[
|
||||
SavedSqlFileAppObject,
|
||||
SavedShellFileAppObject,
|
||||
SavedChartFileAppObject,
|
||||
SavedMarkdownFileAppObject,
|
||||
SavedFileAppObject,
|
||||
].forEach(fn => {
|
||||
// @ts-ignore
|
||||
fn.extractKey = data => data.file;
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { findForeignKeyForColumn } from 'dbgate-tools';
|
||||
import React from 'react';
|
||||
import { getColumnIcon } from '../datagrid/ColumnLabel';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import { AppObjectList } from './AppObjectList';
|
||||
|
||||
function ColumnAppObject({ data, commonProps }) {
|
||||
const { columnName, dataType, foreignKey } = data;
|
||||
let extInfo = dataType;
|
||||
if (foreignKey) extInfo += ` -> ${foreignKey.refTableName}`;
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
data={data}
|
||||
title={columnName}
|
||||
extInfo={extInfo}
|
||||
icon={getColumnIcon(data, true)}
|
||||
disableHover
|
||||
/>
|
||||
);
|
||||
}
|
||||
ColumnAppObject.extractKey = ({ columnName }) => columnName;
|
||||
|
||||
export default function SubColumnParamList({ data }) {
|
||||
const { columns } = data;
|
||||
|
||||
return (
|
||||
<AppObjectList
|
||||
list={(columns || []).map(col => ({
|
||||
...col,
|
||||
foreignKey: findForeignKeyForColumn(data, col),
|
||||
}))}
|
||||
AppObjectComponent={ColumnAppObject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
11
packages/web/src/appobj/SubDatabaseList.svelte
Normal file
11
packages/web/src/appobj/SubDatabaseList.svelte
Normal 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} />
|
||||
@@ -1,99 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { SelectField } from '../utility/inputs';
|
||||
import ErrorInfo from '../widgets/ErrorInfo';
|
||||
import styled from 'styled-components';
|
||||
import { TextCellViewWrap, TextCellViewNoWrap } from './TextCellView';
|
||||
import JsonCellView from './JsonCellDataView';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const Toolbar = styled.div`
|
||||
display: flex;
|
||||
background: ${props => props.theme.toolbar_background};
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const MainWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const DataWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const formats = [
|
||||
{
|
||||
type: 'textWrap',
|
||||
title: 'Text (wrap)',
|
||||
Component: TextCellViewWrap,
|
||||
single: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
title: 'Text (no wrap)',
|
||||
Component: TextCellViewNoWrap,
|
||||
single: true,
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
title: 'Json',
|
||||
Component: JsonCellView,
|
||||
single: true,
|
||||
},
|
||||
];
|
||||
|
||||
function autodetect(selection, grider, value) {
|
||||
if (_.isString(value)) {
|
||||
if (value.startsWith('[') || value.startsWith('{')) return 'json';
|
||||
}
|
||||
return 'textWrap';
|
||||
}
|
||||
|
||||
export default function CellDataView({ selection = undefined, grider = undefined, selectedValue = undefined }) {
|
||||
const [selectedFormatType, setSelectedFormatType] = React.useState('autodetect');
|
||||
const theme = useTheme();
|
||||
let value = null;
|
||||
if (grider && selection && selection.length == 1) {
|
||||
const rowData = grider.getRowData(selection[0].row);
|
||||
const { column } = selection[0];
|
||||
if (rowData) value = rowData[column];
|
||||
}
|
||||
if (selectedValue) {
|
||||
value = selectedValue;
|
||||
}
|
||||
const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]);
|
||||
const autodetectFormat = formats.find(x => x.type == autodetectFormatType);
|
||||
|
||||
const usedFormatType = selectedFormatType == 'autodetect' ? autodetectFormatType : selectedFormatType;
|
||||
const usedFormat = formats.find(x => x.type == usedFormatType);
|
||||
|
||||
const { Component } = usedFormat || {};
|
||||
|
||||
return (
|
||||
<MainWrapper>
|
||||
<Toolbar theme={theme}>
|
||||
Format:
|
||||
<SelectField value={selectedFormatType} onChange={e => setSelectedFormatType(e.target.value)}>
|
||||
<option value="autodetect">Autodetect - {autodetectFormat.title}</option>
|
||||
|
||||
{formats.map(fmt => (
|
||||
<option value={fmt.type} key={fmt.type}>
|
||||
{fmt.title}
|
||||
</option>
|
||||
))}
|
||||
</SelectField>
|
||||
</Toolbar>
|
||||
|
||||
<DataWrapper>
|
||||
{usedFormat == null || (usedFormat.single && value == null) ? (
|
||||
<ErrorInfo message="Must be selected one cell" />
|
||||
) : (
|
||||
<Component value={value} grider={grider} selection={selection} />
|
||||
)}
|
||||
</DataWrapper>
|
||||
</MainWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ReactJson from 'react-json-view';
|
||||
import ErrorInfo from '../widgets/ErrorInfo';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const OuterWrapper = styled.div`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const InnerWrapper = styled.div`
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
`;
|
||||
|
||||
export default function JsonCellView({ value }) {
|
||||
const theme = useTheme();
|
||||
try {
|
||||
const json = React.useMemo(() => JSON.parse(value), [value]);
|
||||
return (
|
||||
<OuterWrapper>
|
||||
<InnerWrapper>
|
||||
<ReactJson src={json} theme={theme.jsonViewerTheme} />
|
||||
</InnerWrapper>
|
||||
</OuterWrapper>
|
||||
);
|
||||
} catch (err) {
|
||||
return <ErrorInfo message="Error parsing JSON" />;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledInput = styled.textarea`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export function TextCellViewWrap({ value, grider, selection }) {
|
||||
return <StyledInput value={value} wrap="hard" readOnly />;
|
||||
}
|
||||
|
||||
export function TextCellViewNoWrap({ value, grider, selection }) {
|
||||
return (
|
||||
<StyledInput
|
||||
value={value}
|
||||
wrap="off"
|
||||
readOnly
|
||||
// readOnly={grider ? !grider.editable : true}
|
||||
// onChange={(e) => grider.setCellValue(selection[0].row, selection[0].column, e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import React from 'react';
|
||||
import Chart from 'react-chartjs-2';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import useDimensions from '../utility/useDimensions';
|
||||
import { HorizontalSplitter } from '../widgets/Splitter';
|
||||
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
|
||||
import { FormCheckboxField, FormSelectField, FormTextField } from '../utility/forms';
|
||||
import DataChart from './DataChart';
|
||||
import { FormProviderCore } from '../utility/FormProvider';
|
||||
import { loadChartData, loadChartStructure } from './chartDataLoader';
|
||||
import useExtensions from '../utility/useExtensions';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import { FormFieldTemplateTiny } from '../utility/formStyle';
|
||||
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
|
||||
import { presetPrimaryColors } from '@ant-design/colors';
|
||||
import ErrorInfo from '../widgets/ErrorInfo';
|
||||
|
||||
const LeftContainer = styled.div`
|
||||
background-color: ${props => props.theme.manager_background};
|
||||
display: flex;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export default function ChartEditor({ data, config, setConfig, sql, conid, database }) {
|
||||
const [managerSize, setManagerSize] = React.useState(0);
|
||||
const theme = useTheme();
|
||||
const extensions = useExtensions();
|
||||
const [error, setError] = React.useState(null);
|
||||
|
||||
const [availableColumnNames, setAvailableColumnNames] = React.useState([]);
|
||||
const [loadedData, setLoadedData] = React.useState(null);
|
||||
|
||||
const getDriver = async () => {
|
||||
const conn = await getConnectionInfo({ conid });
|
||||
if (!conn) return;
|
||||
const driver = findEngineDriver(conn, extensions);
|
||||
return driver;
|
||||
};
|
||||
|
||||
const handleLoadColumns = async () => {
|
||||
const driver = await getDriver();
|
||||
if (!driver) return;
|
||||
try {
|
||||
const columns = await loadChartStructure(driver, conid, database, sql);
|
||||
setAvailableColumnNames(columns);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadData = async () => {
|
||||
const driver = await getDriver();
|
||||
if (!driver) return;
|
||||
const loaded = await loadChartData(driver, conid, database, sql, config);
|
||||
if (!loaded) return;
|
||||
const { columns, rows } = loaded;
|
||||
setLoadedData({
|
||||
structure: columns,
|
||||
rows,
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (sql && conid && database) {
|
||||
handleLoadColumns();
|
||||
}
|
||||
}, [sql, conid, database, extensions]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data) {
|
||||
setAvailableColumnNames(data ? data.structure.columns.map(x => x.columnName) : []);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (config.labelColumn && sql && conid && database) {
|
||||
handleLoadData();
|
||||
}
|
||||
}, [config, sql, conid, database, availableColumnNames]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<ErrorInfo message={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProviderCore values={config} setValues={setConfig} template={FormFieldTemplateTiny}>
|
||||
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
|
||||
<LeftContainer theme={theme}>
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Style" name="style" height="40%">
|
||||
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
|
||||
<FormSelectField label="Chart type" name="chartType">
|
||||
<option value="bar">Bar</option>
|
||||
<option value="line">Line</option>
|
||||
{/* <option value="radar">Radar</option> */}
|
||||
<option value="pie">Pie</option>
|
||||
<option value="polarArea">Polar area</option>
|
||||
{/* <option value="bubble">Bubble</option>
|
||||
<option value="scatter">Scatter</option> */}
|
||||
</FormSelectField>
|
||||
<FormTextField label="Color set" name="colorSeed" />
|
||||
<FormSelectField label="Truncate from" name="truncateFrom">
|
||||
<option value="begin">Begin</option>
|
||||
<option value="end">End (most recent data for datetime)</option>
|
||||
</FormSelectField>
|
||||
<FormTextField label="Truncate limit" name="truncateLimit" />
|
||||
<FormCheckboxField label="Show relative values" name="showRelativeValues" />
|
||||
</ManagerInnerContainer>
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Data" name="data">
|
||||
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
|
||||
{availableColumnNames.length > 0 && (
|
||||
<FormSelectField label="Label column" name="labelColumn">
|
||||
<option value=""></option>
|
||||
{availableColumnNames.map(col => (
|
||||
<option value={col} key={col}>
|
||||
{col}
|
||||
</option>
|
||||
))}
|
||||
</FormSelectField>
|
||||
)}
|
||||
{availableColumnNames.map(col => (
|
||||
<React.Fragment key={col}>
|
||||
<FormCheckboxField label={col} name={`dataColumn_${col}`} />
|
||||
{config[`dataColumn_${col}`] && (
|
||||
<FormSelectField label="Color" name={`dataColumnColor_${col}`}>
|
||||
<option value="">Random</option>
|
||||
|
||||
{_.keys(presetPrimaryColors).map(color => (
|
||||
<option value={color} key={color}>
|
||||
{_.startCase(color)}
|
||||
</option>
|
||||
))}
|
||||
</FormSelectField>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ManagerInnerContainer>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
</LeftContainer>
|
||||
|
||||
<DataChart data={data || loadedData} />
|
||||
</HorizontalSplitter>
|
||||
</FormProviderCore>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import ToolbarButton from '../widgets/ToolbarButton';
|
||||
|
||||
export default function ChartToolbar({ modelState, dispatchModel }) {
|
||||
return (
|
||||
<>
|
||||
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
|
||||
Undo
|
||||
</ToolbarButton>
|
||||
<ToolbarButton disabled={!modelState.canRedo} onClick={() => dispatchModel({ type: 'redo' })} icon="icon redo">
|
||||
Redo
|
||||
</ToolbarButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import Chart from 'react-chartjs-2';
|
||||
import randomcolor from 'randomcolor';
|
||||
import styled from 'styled-components';
|
||||
import useDimensions from '../utility/useDimensions';
|
||||
import { useForm } from '../utility/FormProvider';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import moment from 'moment';
|
||||
|
||||
const ChartWrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
function getTimeAxis(labels) {
|
||||
const res = [];
|
||||
for (const label of labels) {
|
||||
const parsed = moment(label);
|
||||
if (!parsed.isValid()) return null;
|
||||
const iso = parsed.toISOString();
|
||||
if (iso < '1850-01-01T00:00:00' || iso > '2150-01-01T00:00:00') return null;
|
||||
res.push(parsed);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getLabels(labelValues, timeAxis, chartType) {
|
||||
if (!timeAxis) return labelValues;
|
||||
if (chartType === 'line') return timeAxis.map(x => x.toDate());
|
||||
return timeAxis.map(x => x.format('D. M. YYYY'));
|
||||
}
|
||||
|
||||
function getOptions(timeAxis, chartType) {
|
||||
if (timeAxis && chartType === 'line') {
|
||||
return {
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
type: 'time',
|
||||
distribution: 'linear',
|
||||
|
||||
time: {
|
||||
tooltipFormat: 'D. M. YYYY HH:mm',
|
||||
displayFormats: {
|
||||
millisecond: 'HH:mm:ss.SSS',
|
||||
second: 'HH:mm:ss',
|
||||
minute: 'HH:mm',
|
||||
hour: 'D.M hA',
|
||||
day: 'D. M.',
|
||||
week: 'D. M. YYYY',
|
||||
month: 'MM-YYYY',
|
||||
quarter: '[Q]Q - YYYY',
|
||||
year: 'YYYY',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function createChartData(freeData, labelColumn, dataColumns, colorSeed, chartType, dataColumnColors, theme) {
|
||||
if (!freeData || !labelColumn || !dataColumns || !freeData.rows || dataColumns.length == 0) return [{}, {}];
|
||||
const colors = randomcolor({
|
||||
count: _.max([freeData.rows.length, dataColumns.length, 1]),
|
||||
seed: colorSeed,
|
||||
});
|
||||
let backgroundColor = null;
|
||||
let borderColor = null;
|
||||
const labelValues = freeData.rows.map(x => x[labelColumn]);
|
||||
const timeAxis = getTimeAxis(labelValues);
|
||||
const labels = getLabels(labelValues, timeAxis, chartType);
|
||||
const res = {
|
||||
labels,
|
||||
datasets: dataColumns.map((dataColumn, columnIndex) => {
|
||||
if (chartType == 'line' || chartType == 'bar') {
|
||||
const color = dataColumnColors[dataColumn];
|
||||
if (color) {
|
||||
backgroundColor = theme.main_palettes[color][4] + '80';
|
||||
borderColor = theme.main_palettes[color][7];
|
||||
} else {
|
||||
backgroundColor = colors[columnIndex] + '80';
|
||||
borderColor = colors[columnIndex];
|
||||
}
|
||||
} else {
|
||||
backgroundColor = colors;
|
||||
}
|
||||
|
||||
return {
|
||||
label: dataColumn,
|
||||
data: freeData.rows.map(row => row[dataColumn]),
|
||||
backgroundColor,
|
||||
borderColor,
|
||||
borderWidth: 1,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const options = getOptions(timeAxis, chartType);
|
||||
return [res, options];
|
||||
}
|
||||
|
||||
export function extractDataColumns(values) {
|
||||
const dataColumns = [];
|
||||
for (const key in values) {
|
||||
if (key.startsWith('dataColumn_') && values[key]) {
|
||||
dataColumns.push(key.substring('dataColumn_'.length));
|
||||
}
|
||||
}
|
||||
return dataColumns;
|
||||
}
|
||||
export function extractDataColumnColors(values, dataColumns) {
|
||||
const res = {};
|
||||
for (const column of dataColumns) {
|
||||
const color = values[`dataColumnColor_${column}`];
|
||||
if (color) res[column] = color;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export default function DataChart({ data }) {
|
||||
const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions();
|
||||
const { values } = useForm();
|
||||
const theme = useTheme();
|
||||
|
||||
const { labelColumn } = values;
|
||||
const dataColumns = extractDataColumns(values);
|
||||
const dataColumnColors = extractDataColumnColors(values, dataColumns);
|
||||
const [chartData, options] = createChartData(
|
||||
data,
|
||||
labelColumn,
|
||||
dataColumns,
|
||||
values.colorSeed || '5',
|
||||
values.chartType,
|
||||
dataColumnColors,
|
||||
theme
|
||||
);
|
||||
|
||||
return (
|
||||
<ChartWrapper ref={containerRef}>
|
||||
<Chart
|
||||
key={`${values.chartType}|${containerWidth}|${containerHeight}`}
|
||||
width={containerWidth}
|
||||
height={containerHeight}
|
||||
data={chartData}
|
||||
type={values.chartType}
|
||||
options={{
|
||||
...options,
|
||||
// elements: {
|
||||
// point: {
|
||||
// radius: 0,
|
||||
// },
|
||||
// },
|
||||
// tooltips: {
|
||||
// mode: 'index',
|
||||
// intersect: false,
|
||||
// },
|
||||
}}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { dumpSqlSelect, Select } from 'dbgate-sqltree';
|
||||
import { EngineDriver } from 'dbgate-types';
|
||||
import axios from '../utility/axios';
|
||||
import _ from 'lodash';
|
||||
import { extractDataColumns } from './DataChart';
|
||||
|
||||
export async function loadChartStructure(driver: EngineDriver, conid, database, sql) {
|
||||
const select: Select = {
|
||||
commandType: 'select',
|
||||
selectAll: true,
|
||||
topRecords: 1,
|
||||
from: {
|
||||
subQueryString: sql,
|
||||
alias: 'subq',
|
||||
},
|
||||
};
|
||||
|
||||
const dmp = driver.createDumper();
|
||||
dumpSqlSelect(dmp, select);
|
||||
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
||||
if (resp.data.errorMessage) throw new Error(resp.data.errorMessage);
|
||||
return resp.data.columns.map(x => x.columnName);
|
||||
}
|
||||
|
||||
export async function loadChartData(driver: EngineDriver, conid, database, sql, config) {
|
||||
const dataColumns = extractDataColumns(config);
|
||||
const { labelColumn, truncateFrom, truncateLimit, showRelativeValues } = config;
|
||||
if (!labelColumn || !dataColumns || dataColumns.length == 0) return null;
|
||||
|
||||
const select: Select = {
|
||||
commandType: 'select',
|
||||
|
||||
columns: [
|
||||
{
|
||||
exprType: 'column',
|
||||
source: { alias: 'subq' },
|
||||
columnName: labelColumn,
|
||||
alias: labelColumn,
|
||||
},
|
||||
// @ts-ignore
|
||||
...dataColumns.map(columnName => ({
|
||||
exprType: 'call',
|
||||
func: 'SUM',
|
||||
args: [
|
||||
{
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source: { alias: 'subq' },
|
||||
},
|
||||
],
|
||||
alias: columnName,
|
||||
})),
|
||||
],
|
||||
topRecords: truncateLimit || 100,
|
||||
from: {
|
||||
subQueryString: sql,
|
||||
alias: 'subq',
|
||||
},
|
||||
groupBy: [
|
||||
{
|
||||
exprType: 'column',
|
||||
source: { alias: 'subq' },
|
||||
columnName: labelColumn,
|
||||
},
|
||||
],
|
||||
orderBy: [
|
||||
{
|
||||
exprType: 'column',
|
||||
source: { alias: 'subq' },
|
||||
columnName: labelColumn,
|
||||
direction: truncateFrom == 'end' ? 'DESC' : 'ASC',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dmp = driver.createDumper();
|
||||
dumpSqlSelect(dmp, select);
|
||||
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
||||
let { rows, columns } = resp.data;
|
||||
if (truncateFrom == 'end' && rows) {
|
||||
rows = _.reverse([...rows]);
|
||||
}
|
||||
if (showRelativeValues) {
|
||||
const maxValues = dataColumns.map(col => _.max(rows.map(row => row[col])));
|
||||
for (const [col, max] of _.zip(dataColumns, maxValues)) {
|
||||
if (!max) continue;
|
||||
if (!_.isNumber(max)) continue;
|
||||
if (!(max > 0)) continue;
|
||||
rows = rows.map(row => ({
|
||||
...row,
|
||||
[col]: (row[col] / max) * 100,
|
||||
}));
|
||||
// columns = columns.map((x) => {
|
||||
// if (x.columnName == col) {
|
||||
// return { columnName: `${col} %` };
|
||||
// }
|
||||
// return x;
|
||||
// });
|
||||
}
|
||||
}
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
26
packages/web/src/commands/CommandListener.svelte
Normal file
26
packages/web/src/commands/CommandListener.svelte
Normal 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} />
|
||||
105
packages/web/src/commands/CommandPalette.svelte
Normal file
105
packages/web/src/commands/CommandPalette.svelte
Normal 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>
|
||||
46
packages/web/src/commands/registerCommand.ts
Normal file
46
packages/web/src/commands/registerCommand.ts
Normal 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,
|
||||
},
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
9
packages/web/src/commands/runCommand.ts
Normal file
9
packages/web/src/commands/runCommand.ts
Normal 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();
|
||||
}
|
||||
41
packages/web/src/commands/stdCommands.ts
Normal file
41
packages/web/src/commands/stdCommands.ts
Normal 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),
|
||||
});
|
||||
@@ -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];
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
61
packages/web/src/datagrid/ColumnHeaderControl.svelte
Normal file
61
packages/web/src/datagrid/ColumnHeaderControl.svelte
Normal 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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
46
packages/web/src/datagrid/ColumnLabel.svelte
Normal file
46
packages/web/src/datagrid/ColumnLabel.svelte
Normal 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>
|
||||
@@ -1,94 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ColumnLabel from './ColumnLabel';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { ExpandIcon } from '../icons';
|
||||
import InlineButton from '../widgets/InlineButton';
|
||||
import { ManagerInnerContainer } from './ManagerStyles';
|
||||
import SearchInput from '../widgets/SearchInput';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
const Row = styled.div`
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
background-color: ${props => props.theme.manager_background_blue[1]};
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchBoxWrapper = styled.div`
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
// -webkit-appearance: none;
|
||||
// -moz-appearance: none;
|
||||
// appearance: none;
|
||||
// width: 50px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* @param {object} props
|
||||
* @param {import('dbgate-datalib').GridDisplay} props.display
|
||||
* @param {import('dbgate-datalib').DisplayColumn} props.column
|
||||
*/
|
||||
function ColumnManagerRow(props) {
|
||||
const { display, column } = props;
|
||||
const [isHover, setIsHover] = React.useState(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Row
|
||||
onMouseEnter={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
theme={theme}
|
||||
onClick={e => {
|
||||
// @ts-ignore
|
||||
if (e.target.closest('.expandColumnIcon')) return;
|
||||
display.focusColumn(column.uniqueName);
|
||||
}}
|
||||
>
|
||||
<ExpandIcon
|
||||
className="expandColumnIcon"
|
||||
isBlank={!column.foreignKey}
|
||||
isExpanded={column.foreignKey && display.isExpandedColumn(column.uniqueName)}
|
||||
onClick={() => display.toggleExpandedColumn(column.uniqueName)}
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{ marginLeft: `${5 + (column.uniquePath.length - 1) * 10}px` }}
|
||||
checked={column.isChecked}
|
||||
onChange={() => display.setColumnVisibility(column.uniquePath, !column.isChecked)}
|
||||
></input>
|
||||
<ColumnLabel {...column} />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
export default function ColumnManager(props) {
|
||||
const { display } = props;
|
||||
const [columnFilter, setColumnFilter] = React.useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search columns" filter={columnFilter} setFilter={setColumnFilter} />
|
||||
<InlineButton onClick={() => display.hideAllColumns()}>Hide</InlineButton>
|
||||
<InlineButton onClick={() => display.showAllColumns()}>Show</InlineButton>
|
||||
</SearchBoxWrapper>
|
||||
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
|
||||
{display
|
||||
.getColumns(columnFilter)
|
||||
.filter(column => filterName(columnFilter, column.columnName))
|
||||
.map(column => (
|
||||
<ColumnManagerRow key={column.uniqueName} display={display} column={column} />
|
||||
))}
|
||||
</ManagerInnerContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,501 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
||||
import styled from 'styled-components';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import { parseFilter, createMultiLineFilter } from 'dbgate-filterparser';
|
||||
import InlineButton from '../widgets/InlineButton';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import FilterMultipleValuesModal from '../modals/FilterMultipleValuesModal';
|
||||
import SetFilterModal from '../modals/SetFilterModal';
|
||||
import { FontIcon } from '../icons';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import { useShowMenu } from '../modals/showMenu';
|
||||
// import { $ } from '../../Utility/jquery';
|
||||
// import autobind from 'autobind-decorator';
|
||||
// import * as React from 'react';
|
||||
|
||||
// import { createMultiLineFilter } from '../../DataLib/FilterTools';
|
||||
// import { ModalDialog } from '../Dialogs';
|
||||
// import { FilterDialog } from '../Dialogs/FilterDialog';
|
||||
// import { FilterMultipleValuesDialog } from '../Dialogs/FilterMultipleValuesDialog';
|
||||
// import { IconSpan } from '../Navigation/NavUtils';
|
||||
// import { KeyCodes } from '../ReactDataGrid/KeyCodes';
|
||||
// import { DropDownMenu, DropDownMenuDivider, DropDownMenuItem, DropDownSubmenuItem } from './DropDownMenu';
|
||||
// import { FilterParserType } from '../../SwaggerClients';
|
||||
// import { IFilterHolder } from '../CommonControls';
|
||||
// import { GrayFilterIcon } from '../Icons';
|
||||
|
||||
// export interface IDataFilterControlProps {
|
||||
// filterType: FilterParserType;
|
||||
// getFilter: Function;
|
||||
// setFilter: Function;
|
||||
// width: number;
|
||||
// onControlKey?: Function;
|
||||
// isReadOnly?: boolean;
|
||||
// inputElementId?: string;
|
||||
// }
|
||||
|
||||
const FilterDiv = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
const FilterInput = styled.input`
|
||||
flex: 1;
|
||||
min-width: 10px;
|
||||
background-color: ${props =>
|
||||
props.state == 'ok'
|
||||
? props.theme.input_background_green[1]
|
||||
: props.state == 'error'
|
||||
? props.theme.input_background_red[1]
|
||||
: props.theme.input_background};
|
||||
`;
|
||||
// const FilterButton = styled.button`
|
||||
// color: gray;
|
||||
// `;
|
||||
|
||||
function DropDownContent({ filterType, setFilter, filterMultipleValues, openFilterWindow }) {
|
||||
switch (filterType) {
|
||||
case 'number':
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('=')}>Equals...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('>')}>Greater Than...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('>=')}>Greater Than Or Equal To...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('<')}>Less Than...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('<=')}>Less Than Or Equal To...</DropDownMenuItem>
|
||||
</>
|
||||
);
|
||||
case 'logical':
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('TRUE')}>Is True</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('FALSE')}>Is False</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('TRUE, NULL')}>Is True or NULL</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('FALSE, NULL')}>Is False or NULL</DropDownMenuItem>
|
||||
</>
|
||||
);
|
||||
case 'datetime':
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
||||
|
||||
<DropDownMenuDivider />
|
||||
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('<=')}>Before...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('>=')}>After...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('>=;<=')}>Between...</DropDownMenuItem>
|
||||
|
||||
<DropDownMenuDivider />
|
||||
|
||||
<DropDownMenuItem onClick={x => setFilter('TOMORROW')}>Tomorrow</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('TODAY')}>Today</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('YESTERDAY')}>Yesterday</DropDownMenuItem>
|
||||
|
||||
<DropDownMenuDivider />
|
||||
|
||||
<DropDownMenuItem onClick={x => setFilter('NEXT WEEK')}>Next Week</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('THIS WEEK')}>This Week</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('LAST WEEK')}>Last Week</DropDownMenuItem>
|
||||
|
||||
<DropDownMenuDivider />
|
||||
|
||||
<DropDownMenuItem onClick={x => setFilter('NEXT MONTH')}>Next Month</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('THIS MONTH')}>This Month</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('LAST MONTH')}>Last Month</DropDownMenuItem>
|
||||
|
||||
<DropDownMenuDivider />
|
||||
|
||||
<DropDownMenuItem onClick={x => setFilter('NEXT YEAR')}>Next Year</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('THIS YEAR')}>This Year</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('LAST YEAR')}>Last Year</DropDownMenuItem>
|
||||
|
||||
<DropDownMenuDivider />
|
||||
|
||||
{/* <DropDownSubmenuItem title="All dates in period">
|
||||
<DropDownMenuItem onClick={x => setFilter('JAN')}>January</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('FEB')}>February</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('MAR')}>March</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('APR')}>April</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('JUN')}>June</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('JUL')}>July</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('AUG')}>August</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('SEP')}>September</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('OCT')}>October</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NOV')}>November</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('DEC')}>December</DropDownMenuItem>
|
||||
|
||||
<DropDownMenuDivider />
|
||||
|
||||
<DropDownMenuItem onClick={x => setFilter('MON')}>Monday</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('TUE')}>Tuesday</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('WED')}>Wednesday</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('THU')}>Thursday</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('FRI')}>Friday</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('SAT')}>Saturday</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('SUN')}>Sunday</DropDownMenuItem>
|
||||
</DropDownSubmenuItem> */}
|
||||
</>
|
||||
);
|
||||
case 'string':
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
||||
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('=')}>Equals...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('EMPTY, NULL')}>Is Empty Or Null</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value</DropDownMenuItem>
|
||||
|
||||
<DropDownMenuDivider />
|
||||
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('+')}>Contains...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('~')}>Does Not Contain...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('^')}>Begins With...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('!^')}>Does Not Begin With...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('$')}>Ends With...</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={x => openFilterWindow('!$')}>Does Not End With...</DropDownMenuItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function DataFilterControl({
|
||||
isReadOnly = false,
|
||||
filterType,
|
||||
filter,
|
||||
setFilter,
|
||||
focusIndex = 0,
|
||||
onFocusGrid = undefined,
|
||||
}) {
|
||||
const showModal = useShowModal();
|
||||
const showMenu = useShowMenu();
|
||||
const theme = useTheme();
|
||||
const [filterState, setFilterState] = React.useState('empty');
|
||||
const setFilterText = filter => {
|
||||
setFilter(filter);
|
||||
editorRef.current.value = filter || '';
|
||||
updateFilterState();
|
||||
};
|
||||
const applyFilter = () => {
|
||||
if ((filter || '') == (editorRef.current.value || '')) return;
|
||||
setFilter(editorRef.current.value);
|
||||
};
|
||||
const filterMultipleValues = () => {
|
||||
showModal(modalState => (
|
||||
<FilterMultipleValuesModal
|
||||
modalState={modalState}
|
||||
onFilter={(mode, text) => setFilterText(createMultiLineFilter(mode, text))}
|
||||
/>
|
||||
));
|
||||
};
|
||||
const openFilterWindow = operator => {
|
||||
showModal(modalState => (
|
||||
<SetFilterModal
|
||||
filterType={filterType}
|
||||
modalState={modalState}
|
||||
onFilter={text => setFilterText(text)}
|
||||
condition1={operator}
|
||||
/>
|
||||
));
|
||||
};
|
||||
const buttonRef = React.useRef();
|
||||
const editorRef = React.useRef();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (focusIndex) editorRef.current.focus();
|
||||
}, [focusIndex]);
|
||||
|
||||
const handleKeyDown = ev => {
|
||||
if (isReadOnly) return;
|
||||
if (ev.keyCode == keycodes.enter) {
|
||||
applyFilter();
|
||||
}
|
||||
if (ev.keyCode == keycodes.escape) {
|
||||
setFilterText('');
|
||||
}
|
||||
if (ev.keyCode == keycodes.downArrow) {
|
||||
if (onFocusGrid) onFocusGrid();
|
||||
// ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
|
||||
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
|
||||
// }
|
||||
};
|
||||
|
||||
const updateFilterState = () => {
|
||||
const value = editorRef.current.value;
|
||||
try {
|
||||
if (value) {
|
||||
parseFilter(value, filterType);
|
||||
setFilterState('ok');
|
||||
} else {
|
||||
setFilterState('empty');
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log('PARSE ERROR', err);
|
||||
setFilterState('error');
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
editorRef.current.value = filter || '';
|
||||
updateFilterState();
|
||||
}, [filter]);
|
||||
|
||||
const handleShowMenu = () => {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
showMenu(
|
||||
rect.left,
|
||||
rect.bottom,
|
||||
<DropDownContent
|
||||
filterType={filterType}
|
||||
setFilter={setFilterText}
|
||||
filterMultipleValues={filterMultipleValues}
|
||||
openFilterWindow={openFilterWindow}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function handlePaste(event) {
|
||||
var pastedText = undefined;
|
||||
// @ts-ignore
|
||||
if (window.clipboardData && window.clipboardData.getData) {
|
||||
// IE
|
||||
// @ts-ignore
|
||||
pastedText = window.clipboardData.getData('Text');
|
||||
} else if (event.clipboardData && event.clipboardData.getData) {
|
||||
pastedText = event.clipboardData.getData('text/plain');
|
||||
}
|
||||
if (pastedText && pastedText.includes('\n')) {
|
||||
event.preventDefault();
|
||||
setFilterText(createMultiLineFilter('is', pastedText));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterDiv>
|
||||
<FilterInput
|
||||
theme={theme}
|
||||
ref={editorRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
type="text"
|
||||
readOnly={isReadOnly}
|
||||
onChange={updateFilterState}
|
||||
state={filterState}
|
||||
onBlur={applyFilter}
|
||||
onPaste={handlePaste}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<InlineButton buttonRef={buttonRef} onClick={handleShowMenu} square>
|
||||
<FontIcon icon="icon filter" />
|
||||
</InlineButton>
|
||||
</FilterDiv>
|
||||
);
|
||||
}
|
||||
// domEditor: Element;
|
||||
|
||||
// @autobind
|
||||
// applyFilter() {
|
||||
// this.props.setFilter($(this.domEditor).val());
|
||||
// }
|
||||
|
||||
// @autobind
|
||||
// clearFilter() {
|
||||
// $(this.domEditor).val('');
|
||||
// this.applyFilter();
|
||||
// }
|
||||
|
||||
// setFilter(value: string) {
|
||||
// $(this.domEditor).val(value);
|
||||
// this.applyFilter();
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// let dropDownContent = null;
|
||||
|
||||
// let filterIconSpan = <span className='fa fa-filter' style={{color: 'gray', display: 'inline-block', width: '8px', height: '0', whiteSpace: 'nowrap'}} ></span>;
|
||||
// //filterIconSpan = null;
|
||||
|
||||
// if (this.props.filterType == 'Number') {
|
||||
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('=')}>Equals...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('>')}>Greater Than...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=')}>Greater Than Or Equal To...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<')}>Less Than...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<=')}>Less Than Or Equal To...</DropDownMenuItem>
|
||||
// </DropDownMenu>;
|
||||
// }
|
||||
|
||||
// if (this.props.filterType == 'Logical') {
|
||||
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('TRUE')}>Is True</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('FALSE')}>Is False</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('TRUE, NULL')}>Is True or NULL</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('FALSE, NULL')}>Is False or NULL</DropDownMenuItem>
|
||||
// </DropDownMenu>;
|
||||
// }
|
||||
|
||||
// if (this.props.filterType == 'DateTime') {
|
||||
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
||||
|
||||
// <DropDownMenuDivider />
|
||||
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<=')}>Before...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=')}>After...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=;<=')}>Between...</DropDownMenuItem>
|
||||
|
||||
// <DropDownMenuDivider />
|
||||
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('TOMORROW')}>Tomorrow</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('TODAY')}>Today</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('YESTERDAY')}>Yesterday</DropDownMenuItem>
|
||||
|
||||
// <DropDownMenuDivider />
|
||||
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NEXT WEEK')}>Next Week</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('THIS WEEK')}>This Week</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('LAST WEEK')}>Last Week</DropDownMenuItem>
|
||||
|
||||
// <DropDownMenuDivider />
|
||||
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NEXT MONTH')}>Next Month</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('THIS MONTH')}>This Month</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('LAST MONTH')}>Last Month</DropDownMenuItem>
|
||||
|
||||
// <DropDownMenuDivider />
|
||||
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NEXT YEAR')}>Next Year</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('THIS YEAR')}>This Year</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('LAST YEAR')}>Last Year</DropDownMenuItem>
|
||||
|
||||
// <DropDownMenuDivider />
|
||||
|
||||
// <DropDownSubmenuItem title='All dates in period'>
|
||||
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('JAN')}>January</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('FEB')}>February</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('MAR')}>March</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('APR')}>April</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('JUN')}>June</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('JUL')}>July</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('AUG')}>August</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('SEP')}>September</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('OCT')}>October</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NOV')}>November</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('DEC')}>December</DropDownMenuItem>
|
||||
|
||||
// <DropDownMenuDivider />
|
||||
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('MON')}>Monday</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('TUE')}>Tuesday</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('WED')}>Wednesday</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('THU')}>Thursday</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('FRI')}>Friday</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('SAT')}>Saturday</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('SUN')}>Sunday</DropDownMenuItem>
|
||||
|
||||
// </DropDownSubmenuItem>
|
||||
// </DropDownMenu>;
|
||||
// }
|
||||
|
||||
// if (this.props.filterType == 'String') {
|
||||
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
|
||||
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('=')}>Equals...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('EMPTY, NULL')}>Is Empty Or Null</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value</DropDownMenuItem>
|
||||
|
||||
// <DropDownMenuDivider />
|
||||
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('+')}>Contains...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('~')}>Does Not Contain...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('^')}>Begins With...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('!^')}>Does Not Begin With...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('$')}>Ends With...</DropDownMenuItem>
|
||||
// <DropDownMenuItem onClick={x => this.openFilterWindow('!$')}>Does Not End With...</DropDownMenuItem>
|
||||
// </DropDownMenu>;
|
||||
// }
|
||||
|
||||
// if (this.props.isReadOnly) {
|
||||
// dropDownContent = <GrayFilterIcon style={{marginLeft: '5px'}} />;
|
||||
// }
|
||||
|
||||
// return <div style={{ minWidth: `${this.props.width}px`, maxWidth: `${this.props.width}px`, width: `${this.props.width}` }}>
|
||||
// <input id={this.props.inputElementId} type='text' style={{ 'width': `${(this.props.width - 20)}px` }} readOnly={this.props.isReadOnly}
|
||||
// onBlur={this.applyFilter} ref={x => this.setDomEditor(x)} onKeyDown={this.editorKeyDown} placeholder='Search' ></input>
|
||||
|
||||
// {dropDownContent}
|
||||
// </div>;
|
||||
// }
|
||||
|
||||
// async filterMultipleValues() {
|
||||
// let result = await ModalDialog.run(<FilterMultipleValuesDialog header='Filter multiple values' />);
|
||||
// if (!result) return;
|
||||
// let { mode, text } = result;
|
||||
// let filter = createMultiLineFilter(mode, text);
|
||||
// this.setFilter(filter);
|
||||
// }
|
||||
|
||||
// openFilterWindow(selectedOperator: string) {
|
||||
// FilterDialog.runFilter(this, this.props.filterType, selectedOperator);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// setDomEditor(editor) {
|
||||
// this.domEditor = editor;
|
||||
// $(editor).val(this.props.getFilter());
|
||||
// }
|
||||
|
||||
// @autobind
|
||||
// editorKeyDown(ev) {
|
||||
// if (this.props.isReadOnly) return;
|
||||
// if (ev.keyCode == KeyCodes.Enter) {
|
||||
// this.applyFilter();
|
||||
// }
|
||||
// if (ev.keyCode == KeyCodes.Escape) {
|
||||
// this.clearFilter();
|
||||
// }
|
||||
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
|
||||
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
|
||||
// }
|
||||
// }
|
||||
|
||||
// focus() {
|
||||
// $(this.domEditor).focus();
|
||||
// }
|
||||
// }
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ColumnManager from './ColumnManager';
|
||||
import FormViewFilters from '../formview/FormViewFilters';
|
||||
|
||||
import ReferenceManager from './ReferenceManager';
|
||||
import { HorizontalSplitter } from '../widgets/Splitter';
|
||||
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
|
||||
import CellDataView from '../celldata/CellDataView';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const LeftContainer = styled.div`
|
||||
background-color: ${props => props.theme.manager_background};
|
||||
display: flex;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const DataGridContainer = styled.div`
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export default function DataGrid(props) {
|
||||
const { GridCore, FormView, formDisplay } = props;
|
||||
const theme = useTheme();
|
||||
const [managerSize, setManagerSize] = React.useState(0);
|
||||
const [selection, setSelection] = React.useState([]);
|
||||
const [formSelection, setFormSelection] = React.useState(null);
|
||||
const [grider, setGrider] = React.useState(null);
|
||||
const [collapsedWidgets, setCollapsedWidgets] = React.useState([]);
|
||||
// const [formViewData, setFormViewData] = React.useState(null);
|
||||
const isFormView = !!(formDisplay && formDisplay.config && formDisplay.config.isFormView);
|
||||
|
||||
return (
|
||||
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
|
||||
<LeftContainer theme={theme}>
|
||||
<WidgetColumnBar onChangeCollapsedWidgets={setCollapsedWidgets}>
|
||||
{!isFormView && (
|
||||
<WidgetColumnBarItem title="Columns" name="columns" height={props.showReferences ? '40%' : '60%'}>
|
||||
<ColumnManager {...props} managerSize={managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
)}
|
||||
{isFormView && (
|
||||
<WidgetColumnBarItem title="Filters" name="filters" height="30%">
|
||||
<FormViewFilters {...props} managerSize={managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
)}
|
||||
{props.showReferences && props.display.hasReferences && (
|
||||
<WidgetColumnBarItem title="References" name="references" height="30%" collapsed={props.isDetailView}>
|
||||
<ReferenceManager {...props} managerSize={managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
)}
|
||||
<WidgetColumnBarItem
|
||||
title="Cell data"
|
||||
name="cellData"
|
||||
// cell data must be collapsed by default, because of performance reasons
|
||||
// when not collapsed, onSelectionChanged of grid is set and RERENDER of this component is done on every selection change
|
||||
collapsed
|
||||
>
|
||||
{isFormView ? (
|
||||
<CellDataView selectedValue={formSelection} />
|
||||
) : (
|
||||
<CellDataView selection={selection} grider={grider} />
|
||||
)}
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
</LeftContainer>
|
||||
|
||||
<DataGridContainer>
|
||||
{isFormView ? (
|
||||
<FormView {...props} onSelectionChanged={collapsedWidgets.includes('cellData') ? null : setFormSelection} />
|
||||
) : (
|
||||
<GridCore
|
||||
{...props}
|
||||
onSelectionChanged={collapsedWidgets.includes('cellData') ? null : setSelection}
|
||||
onChangeGrider={setGrider}
|
||||
formViewAvailable={!!FormView && !!formDisplay}
|
||||
/>
|
||||
)}
|
||||
</DataGridContainer>
|
||||
</HorizontalSplitter>
|
||||
);
|
||||
}
|
||||
6
packages/web/src/datagrid/DataGrid.svelte
Normal file
6
packages/web/src/datagrid/DataGrid.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
export let config;
|
||||
export let gridCoreComponent;
|
||||
</script>
|
||||
|
||||
<svelte:component this={gridCoreComponent} {...$$props} />
|
||||
134
packages/web/src/datagrid/DataGridCell.svelte
Normal file
134
packages/web/src/datagrid/DataGridCell.svelte
Normal 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>
|
||||
@@ -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
324
packages/web/src/datagrid/DataGridCore.svelte
Normal file
324
packages/web/src/datagrid/DataGridCore.svelte
Normal 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>
|
||||
@@ -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);
|
||||
53
packages/web/src/datagrid/DataGridRow.svelte
Normal file
53
packages/web/src/datagrid/DataGridRow.svelte
Normal 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>
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
packages/web/src/datagrid/HorizontalScrollBar.svelte
Normal file
41
packages/web/src/datagrid/HorizontalScrollBar.svelte
Normal 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`}> </div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
overflow-x: scroll;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,98 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import keycodes from '../utility/keycodes';
|
||||
|
||||
const StyledInput = styled.input`
|
||||
border: 0px solid;
|
||||
outline: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
`;
|
||||
|
||||
export default function InplaceEditor({
|
||||
widthPx,
|
||||
// rowIndex,
|
||||
// uniqueName,
|
||||
// grider,
|
||||
cellValue,
|
||||
inplaceEditorState,
|
||||
dispatchInsplaceEditor,
|
||||
onSetValue,
|
||||
}) {
|
||||
const editorRef = React.useRef();
|
||||
const widthRef = React.useRef(widthPx);
|
||||
const isChangedRef = React.useRef(!!inplaceEditorState.text);
|
||||
React.useEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
editor.value = inplaceEditorState.text || cellValue;
|
||||
editor.focus();
|
||||
if (inplaceEditorState.selectAll) {
|
||||
editor.select();
|
||||
}
|
||||
}, []);
|
||||
function handleBlur() {
|
||||
if (isChangedRef.current) {
|
||||
const editor = editorRef.current;
|
||||
onSetValue(editor.value);
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.current = false;
|
||||
}
|
||||
dispatchInsplaceEditor({ type: 'close' });
|
||||
}
|
||||
if (inplaceEditorState.shouldSave) {
|
||||
const editor = editorRef.current;
|
||||
if (isChangedRef.current) {
|
||||
onSetValue(editor.value);
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.current = false;
|
||||
}
|
||||
editor.blur();
|
||||
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
|
||||
}
|
||||
function handleKeyDown(event) {
|
||||
const editor = editorRef.current;
|
||||
switch (event.keyCode) {
|
||||
case keycodes.escape:
|
||||
isChangedRef.current = false;
|
||||
dispatchInsplaceEditor({ type: 'close' });
|
||||
break;
|
||||
case keycodes.enter:
|
||||
if (isChangedRef.current) {
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
onSetValue(editor.value);
|
||||
isChangedRef.current = false;
|
||||
}
|
||||
editor.blur();
|
||||
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
|
||||
break;
|
||||
case keycodes.s:
|
||||
if (event.ctrlKey) {
|
||||
if (isChangedRef.current) {
|
||||
onSetValue(editor.value);
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.current = false;
|
||||
}
|
||||
event.preventDefault();
|
||||
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<StyledInput
|
||||
onBlur={handleBlur}
|
||||
ref={editorRef}
|
||||
type="text"
|
||||
onChange={() => (isChangedRef.current = true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
width: widthRef.current,
|
||||
minWidth: widthRef.current,
|
||||
maxWidth: widthRef.current,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import React from 'react';
|
||||
import axios from '../utility/axios';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
import useSocket from '../utility/SocketProvider';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import LoadingDataGridCore from './LoadingDataGridCore';
|
||||
import RowsArrayGrider from './RowsArrayGrider';
|
||||
|
||||
async function loadDataPage(props, offset, limit) {
|
||||
const { jslid, display } = props;
|
||||
|
||||
const response = await axios.post('jsldata/get-rows', {
|
||||
jslid,
|
||||
offset,
|
||||
limit,
|
||||
filters: display ? display.compileFilters() : null,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function dataPageAvailable(props) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadRowCount(props) {
|
||||
const { jslid } = props;
|
||||
|
||||
const response = await axios.request({
|
||||
url: 'jsldata/get-stats',
|
||||
method: 'get',
|
||||
params: {
|
||||
jslid,
|
||||
},
|
||||
});
|
||||
return response.data.rowCount;
|
||||
}
|
||||
|
||||
export default function JslDataGridCore(props) {
|
||||
const { jslid } = props;
|
||||
const [changeIndex, setChangeIndex] = React.useState(0);
|
||||
const [rowCountLoaded, setRowCountLoaded] = React.useState(null);
|
||||
|
||||
const showModal = useShowModal();
|
||||
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const socket = useSocket();
|
||||
|
||||
function exportGrid() {
|
||||
const initialValues = {};
|
||||
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
|
||||
if (archiveMatch) {
|
||||
initialValues.sourceStorageType = 'archive';
|
||||
initialValues.sourceArchiveFolder = archiveMatch[1];
|
||||
initialValues.sourceList = [archiveMatch[2]];
|
||||
} else {
|
||||
initialValues.sourceStorageType = 'jsldata';
|
||||
initialValues.sourceJslId = jslid;
|
||||
initialValues.sourceList = ['query-data'];
|
||||
}
|
||||
showModal(modalState => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
|
||||
}
|
||||
|
||||
const handleJslDataStats = React.useCallback(
|
||||
stats => {
|
||||
if (stats.changeIndex < changeIndex) return;
|
||||
setChangeIndex(stats.changeIndex);
|
||||
setRowCountLoaded(stats.rowCount);
|
||||
},
|
||||
[changeIndex]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (jslid && socket) {
|
||||
socket.on(`jsldata-stats-${jslid}`, handleJslDataStats);
|
||||
return () => {
|
||||
socket.off(`jsldata-stats-${jslid}`, handleJslDataStats);
|
||||
};
|
||||
}
|
||||
}, [jslid]);
|
||||
|
||||
return (
|
||||
<LoadingDataGridCore
|
||||
{...props}
|
||||
exportGrid={exportGrid}
|
||||
loadDataPage={loadDataPage}
|
||||
dataPageAvailable={dataPageAvailable}
|
||||
loadRowCount={loadRowCount}
|
||||
rowCountLoaded={rowCountLoaded}
|
||||
loadNextDataToken={changeIndex}
|
||||
onReload={() => setChangeIndex(0)}
|
||||
griderFactory={RowsArrayGrider.factory}
|
||||
griderFactoryDeps={RowsArrayGrider.factoryDeps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import React from 'react';
|
||||
import DataGridCore from './DataGridCore';
|
||||
|
||||
export default function LoadingDataGridCore(props) {
|
||||
const {
|
||||
display,
|
||||
loadDataPage,
|
||||
dataPageAvailable,
|
||||
loadRowCount,
|
||||
loadNextDataToken,
|
||||
onReload,
|
||||
exportGrid,
|
||||
openQuery,
|
||||
griderFactory,
|
||||
griderFactoryDeps,
|
||||
onChangeGrider,
|
||||
rowCountLoaded,
|
||||
} = props;
|
||||
|
||||
const [loadProps, setLoadProps] = React.useState({
|
||||
isLoading: false,
|
||||
loadedRows: [],
|
||||
isLoadedAll: false,
|
||||
loadedTime: new Date().getTime(),
|
||||
allRowCount: null,
|
||||
errorMessage: null,
|
||||
loadNextDataToken: 0,
|
||||
});
|
||||
const { isLoading, loadedRows, isLoadedAll, loadedTime, allRowCount, errorMessage } = loadProps;
|
||||
|
||||
const loadedTimeRef = React.useRef(0);
|
||||
|
||||
const handleLoadRowCount = async () => {
|
||||
const rowCount = await loadRowCount(props);
|
||||
setLoadProps(oldLoadProps => ({
|
||||
...oldLoadProps,
|
||||
allRowCount: rowCount,
|
||||
}));
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
setLoadProps({
|
||||
allRowCount: null,
|
||||
isLoading: false,
|
||||
loadedRows: [],
|
||||
isLoadedAll: false,
|
||||
loadedTime: new Date().getTime(),
|
||||
errorMessage: null,
|
||||
loadNextDataToken: 0,
|
||||
});
|
||||
if (onReload) onReload();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.masterLoadedTime && props.masterLoadedTime > loadedTime) {
|
||||
display.reload();
|
||||
}
|
||||
if (display.cache.refreshTime > loadedTime) {
|
||||
reload();
|
||||
}
|
||||
});
|
||||
|
||||
const loadNextData = async () => {
|
||||
if (isLoading) return;
|
||||
setLoadProps(oldLoadProps => ({
|
||||
...oldLoadProps,
|
||||
isLoading: true,
|
||||
}));
|
||||
const loadStart = new Date().getTime();
|
||||
loadedTimeRef.current = loadStart;
|
||||
|
||||
const nextRows = await loadDataPage(props, loadedRows.length, 100);
|
||||
if (loadedTimeRef.current !== loadStart) {
|
||||
// new load was dispatched
|
||||
return;
|
||||
}
|
||||
// if (!_.isArray(nextRows)) {
|
||||
// console.log('Error loading data from server', nextRows);
|
||||
// nextRows = [];
|
||||
// }
|
||||
// console.log('nextRows', nextRows);
|
||||
if (nextRows.errorMessage) {
|
||||
setLoadProps(oldLoadProps => ({
|
||||
...oldLoadProps,
|
||||
isLoading: false,
|
||||
errorMessage: nextRows.errorMessage,
|
||||
}));
|
||||
} else {
|
||||
if (allRowCount == null) handleLoadRowCount();
|
||||
const loadedInfo = {
|
||||
loadedRows: [...loadedRows, ...nextRows],
|
||||
loadedTime,
|
||||
};
|
||||
setLoadProps(oldLoadProps => ({
|
||||
...oldLoadProps,
|
||||
isLoading: false,
|
||||
isLoadedAll: oldLoadProps.loadNextDataToken == loadNextDataToken && nextRows.length === 0,
|
||||
loadNextDataToken,
|
||||
...loadedInfo,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoadProps(oldProps => ({
|
||||
...oldProps,
|
||||
isLoadedAll: false,
|
||||
}));
|
||||
}, [loadNextDataToken]);
|
||||
|
||||
const griderProps = { ...props, sourceRows: loadedRows };
|
||||
const grider = React.useMemo(() => griderFactory(griderProps), griderFactoryDeps(griderProps));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (onChangeGrider) onChangeGrider(grider);
|
||||
}, [grider]);
|
||||
|
||||
const handleLoadNextData = () => {
|
||||
if (!isLoadedAll && !errorMessage && !grider.disableLoadNextPage) {
|
||||
if (dataPageAvailable(props)) {
|
||||
// If not, callbacks to load missing metadata are dispatched
|
||||
loadNextData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DataGridCore
|
||||
{...props}
|
||||
loadNextData={handleLoadNextData}
|
||||
errorMessage={errorMessage}
|
||||
isLoadedAll={isLoadedAll}
|
||||
loadedTime={loadedTime}
|
||||
exportGrid={exportGrid}
|
||||
allRowCount={rowCountLoaded || allRowCount}
|
||||
openQuery={openQuery}
|
||||
isLoading={isLoading}
|
||||
grider={grider}
|
||||
/>
|
||||
);
|
||||
}
|
||||
92
packages/web/src/datagrid/LoadingDataGridCore.svelte
Normal file
92
packages/web/src/datagrid/LoadingDataGridCore.svelte
Normal 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}
|
||||
/>
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ManagerInnerContainer = styled.div`
|
||||
flex: 1 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
`;
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import ToolbarButton from '../widgets/ToolbarButton';
|
||||
import styled from 'styled-components';
|
||||
import dimensions from '../theme/dimensions';
|
||||
import { FontIcon } from '../icons';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: ${props => props.theme.gridheader_background_cyan[0]};
|
||||
height: ${dimensions.toolBar.height}px;
|
||||
min-height: ${dimensions.toolBar.height}px;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid ${props => props.theme.border};
|
||||
border-bottom: 1px solid ${props => props.theme.border};
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const HeaderText = styled.div`
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
export default function ReferenceHeader({ reference, onClose }) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Container theme={theme}>
|
||||
<Header>
|
||||
<FontIcon icon="img reference" />
|
||||
<HeaderText>
|
||||
{reference.pureName} [{reference.columns.map(x => x.refName).join(', ')}] = master [
|
||||
{reference.columns.map(x => x.baseName).join(', ')}]
|
||||
</HeaderText>
|
||||
</Header>
|
||||
<ToolbarButton icon="icon close" onClick={onClose} patchY={6}>
|
||||
Close
|
||||
</ToolbarButton>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ManagerInnerContainer } from './ManagerStyles';
|
||||
import SearchInput from '../widgets/SearchInput';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { FontIcon } from '../icons';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const SearchBoxWrapper = styled.div`
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const LinkContainer = styled.div`
|
||||
color: ${props => props.theme.manager_font_blue[7]};
|
||||
margin: 5px;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
`;
|
||||
|
||||
const NameContainer = styled.div`
|
||||
margin-left: 5px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
function ManagerRow({ tableName, columns, icon, onClick }) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<LinkContainer onClick={onClick} theme={theme}>
|
||||
<FontIcon icon={icon} />
|
||||
<NameContainer>
|
||||
{tableName} ({columns.map(x => x.columnName).join(', ')})
|
||||
</NameContainer>
|
||||
</LinkContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
export default function ReferenceManager(props) {
|
||||
const [filter, setFilter] = React.useState('');
|
||||
const { display } = props;
|
||||
const { baseTable } = display || {};
|
||||
const { foreignKeys } = baseTable || {};
|
||||
const { dependencies } = baseTable || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search references" filter={filter} setFilter={setFilter} />
|
||||
</SearchBoxWrapper>
|
||||
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
|
||||
{foreignKeys && foreignKeys.length > 0 && (
|
||||
<>
|
||||
<Header>References tables ({foreignKeys.length})</Header>
|
||||
{foreignKeys
|
||||
.filter(fk => filterName(filter, fk.refTableName))
|
||||
.map(fk => (
|
||||
<ManagerRow
|
||||
key={fk.constraintName}
|
||||
icon="img link"
|
||||
tableName={fk.refTableName}
|
||||
columns={fk.columns}
|
||||
onClick={() =>
|
||||
props.onReferenceClick({
|
||||
schemaName: fk.refSchemaName,
|
||||
pureName: fk.refTableName,
|
||||
columns: fk.columns.map(col => ({
|
||||
baseName: col.columnName,
|
||||
refName: col.refColumnName,
|
||||
})),
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{dependencies && dependencies.length > 0 && (
|
||||
<>
|
||||
<Header>Dependend tables ({dependencies.length})</Header>
|
||||
{dependencies
|
||||
.filter(fk => filterName(filter, fk.pureName))
|
||||
.map(fk => (
|
||||
<ManagerRow
|
||||
key={fk.constraintName}
|
||||
icon="img reference"
|
||||
tableName={fk.pureName}
|
||||
columns={fk.columns}
|
||||
onClick={() =>
|
||||
props.onReferenceClick({
|
||||
schemaName: fk.schemaName,
|
||||
pureName: fk.pureName,
|
||||
columns: fk.columns.map(col => ({
|
||||
baseName: col.refColumnName,
|
||||
refName: col.columnName,
|
||||
})),
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ManagerInnerContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
18
packages/web/src/datagrid/RowHeaderCell.svelte
Normal file
18
packages/web/src/datagrid/RowHeaderCell.svelte
Normal 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>
|
||||
@@ -1,20 +0,0 @@
|
||||
import Grider, { GriderRowStatus } from './Grider';
|
||||
|
||||
export default class RowsArrayGrider extends Grider {
|
||||
constructor(private rows: any[]) {
|
||||
super();
|
||||
}
|
||||
getRowData(index: any) {
|
||||
return this.rows[index];
|
||||
}
|
||||
get rowCount() {
|
||||
return this.rows.length;
|
||||
}
|
||||
|
||||
static factory({ sourceRows }): RowsArrayGrider {
|
||||
return new RowsArrayGrider(sourceRows);
|
||||
}
|
||||
static factoryDeps({ sourceRows }) {
|
||||
return [sourceRows];
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import useDimensions from '../utility/useDimensions';
|
||||
|
||||
const StyledHorizontalScrollBar = styled.div`
|
||||
overflow-x: scroll;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
//left: 100px;
|
||||
// right: 20px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
`;
|
||||
|
||||
const StyledHorizontalScrollContent = styled.div``;
|
||||
|
||||
const StyledVerticalScrollBar = styled.div`
|
||||
overflow-y: scroll;
|
||||
width: 20px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 20px;
|
||||
bottom: 16px;
|
||||
// bottom: 0;
|
||||
top: 0;
|
||||
`;
|
||||
|
||||
const StyledVerticalScrollContent = styled.div``;
|
||||
|
||||
export function HorizontalScrollBar({
|
||||
onScroll = undefined,
|
||||
valueToSet = undefined,
|
||||
valueToSetDate = undefined,
|
||||
minimum,
|
||||
maximum,
|
||||
viewportRatio = 0.5,
|
||||
}) {
|
||||
const [ref, { width }, node] = useDimensions();
|
||||
const contentSize = Math.round(width / viewportRatio);
|
||||
|
||||
React.useEffect(() => {
|
||||
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
|
||||
const position = position01 * (contentSize - width);
|
||||
if (node) node.scrollLeft = Math.floor(position);
|
||||
}, [valueToSetDate]);
|
||||
|
||||
const handleScroll = () => {
|
||||
const position = node.scrollLeft;
|
||||
const ratio = position / (contentSize - width);
|
||||
if (ratio < 0) return 0;
|
||||
let res = ratio * (maximum - minimum + 1) + minimum;
|
||||
onScroll(Math.floor(res + 0.3));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHorizontalScrollBar ref={ref} onScroll={handleScroll}>
|
||||
<StyledHorizontalScrollContent style={{ width: `${contentSize}px` }}> </StyledHorizontalScrollContent>
|
||||
</StyledHorizontalScrollBar>
|
||||
);
|
||||
}
|
||||
|
||||
export function VerticalScrollBar({
|
||||
onScroll,
|
||||
valueToSet = undefined,
|
||||
valueToSetDate = undefined,
|
||||
minimum,
|
||||
maximum,
|
||||
viewportRatio = 0.5,
|
||||
}) {
|
||||
const [ref, { height }, node] = useDimensions();
|
||||
const contentSize = Math.round(height / viewportRatio);
|
||||
|
||||
React.useEffect(() => {
|
||||
const position01 = (valueToSet - minimum) / (maximum - minimum + 1);
|
||||
const position = position01 * (contentSize - height);
|
||||
if (node) node.scrollTop = Math.floor(position);
|
||||
}, [valueToSetDate]);
|
||||
|
||||
const handleScroll = () => {
|
||||
const position = node.scrollTop;
|
||||
const ratio = position / (contentSize - height);
|
||||
if (ratio < 0) return 0;
|
||||
let res = ratio * (maximum - minimum + 1) + minimum;
|
||||
onScroll(Math.floor(res + 0.3));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledVerticalScrollBar ref={ref} onScroll={handleScroll}>
|
||||
<StyledVerticalScrollContent style={{ height: `${contentSize}px` }}> </StyledVerticalScrollContent>
|
||||
</StyledVerticalScrollBar>
|
||||
);
|
||||
}
|
||||
|
||||
// export interface IScrollBarProps {
|
||||
// viewportRatio: number;
|
||||
// minimum: number;
|
||||
// maximum: number;
|
||||
// containerStyle: any;
|
||||
// onScroll?: any;
|
||||
// }
|
||||
|
||||
// export abstract class ScrollBarBase extends React.Component<IScrollBarProps, {}> {
|
||||
// domScrollContainer: Element;
|
||||
// domScrollContent: Element;
|
||||
// contentSize: number;
|
||||
// containerResizedBind: any;
|
||||
|
||||
// constructor(props) {
|
||||
// super(props);
|
||||
// this.containerResizedBind = this.containerResized.bind(this);
|
||||
// }
|
||||
|
||||
// componentDidMount() {
|
||||
// $(this.domScrollContainer).scroll(this.onScroll.bind(this));
|
||||
// createResizeDetector(this.domScrollContainer, this.containerResized.bind(this));
|
||||
// window.addEventListener('resize', this.containerResizedBind);
|
||||
// this.updateContentSize();
|
||||
// }
|
||||
|
||||
// componentWillUnmount() {
|
||||
// deleteResizeDetector(this.domScrollContainer);
|
||||
// window.removeEventListener('resize', this.containerResizedBind);
|
||||
// }
|
||||
|
||||
// onScroll() {
|
||||
// if (this.props.onScroll) {
|
||||
// this.props.onScroll(this.value);
|
||||
// }
|
||||
// }
|
||||
|
||||
// get value(): number {
|
||||
// let position = this.getScrollPosition();
|
||||
// let ratio = position / (this.contentSize - this.getContainerSize());
|
||||
// if (ratio < 0) return 0;
|
||||
// let res = ratio * (this.props.maximum - this.props.minimum + 1) + this.props.minimum;
|
||||
// return Math.floor(res + 0.3);
|
||||
// }
|
||||
|
||||
// set value(value: number) {
|
||||
// let position01 = (value - this.props.minimum) / (this.props.maximum - this.props.minimum + 1);
|
||||
// let position = position01 * (this.contentSize - this.getContainerSize());
|
||||
// this.setScrollPosition(Math.floor(position));
|
||||
// }
|
||||
|
||||
// containerResized() {
|
||||
// this.setContentSizeField();
|
||||
// this.updateContentSize();
|
||||
// }
|
||||
|
||||
// setContentSizeField() {
|
||||
// let lastContentSize = this.contentSize;
|
||||
// this.contentSize = Math.round(this.getContainerSize() / this.props.viewportRatio);
|
||||
// if (_.isNaN(this.contentSize)) this.contentSize = 0;
|
||||
// if (this.contentSize > 1000000 && detectBrowser() == BrowserType.Firefox) this.contentSize = 1000000;
|
||||
// if (lastContentSize != this.contentSize) {
|
||||
// this.updateContentSize();
|
||||
// }
|
||||
// }
|
||||
|
||||
// abstract getContainerSize(): number;
|
||||
// abstract updateContentSize();
|
||||
// abstract getScrollPosition(): number;
|
||||
// abstract setScrollPosition(value: number);
|
||||
// }
|
||||
|
||||
// export class HorizontalScrollBar extends ScrollBarBase {
|
||||
// render() {
|
||||
// this.setContentSizeField();
|
||||
|
||||
// return <div className='ReactGridHorizontalScrollBar' ref={x => this.domScrollContainer = x} style={this.props.containerStyle}>
|
||||
// <div className='ReactGridHorizontalScrollContent' ref={x => this.domScrollContent = x} style={{ width: this.contentSize }}>
|
||||
//
|
||||
// </div>
|
||||
// </div>;
|
||||
// }
|
||||
|
||||
// getContainerSize(): number {
|
||||
// return $(this.domScrollContainer).width();
|
||||
// }
|
||||
|
||||
// updateContentSize() {
|
||||
// $(this.domScrollContent).width(this.contentSize);
|
||||
// }
|
||||
|
||||
// getScrollPosition() {
|
||||
// return $(this.domScrollContainer).scrollLeft();
|
||||
// }
|
||||
|
||||
// setScrollPosition(value: number) {
|
||||
// $(this.domScrollContainer).scrollLeft(value);
|
||||
// }
|
||||
// }
|
||||
|
||||
// export class VerticalScrollBar extends ScrollBarBase {
|
||||
// render() {
|
||||
// this.setContentSizeField();
|
||||
|
||||
// return <div className='ReactGridVerticalScrollBar' ref={x => this.domScrollContainer = x} style={this.props.containerStyle}>
|
||||
// <div className='ReactGridVerticalScrollContent' ref={x => this.domScrollContent = x} style={{ height: this.contentSize }}>
|
||||
//
|
||||
// </div>
|
||||
// </div>;
|
||||
// }
|
||||
|
||||
// getContainerSize(): number {
|
||||
// return $(this.domScrollContainer).height();
|
||||
// }
|
||||
|
||||
// updateContentSize() {
|
||||
// $(this.domScrollContent).height(this.contentSize);
|
||||
// }
|
||||
|
||||
// getScrollPosition() {
|
||||
// return $(this.domScrollContainer).scrollTop();
|
||||
// }
|
||||
|
||||
// setScrollPosition(value: number) {
|
||||
// $(this.domScrollContainer).scrollTop(value);
|
||||
// }
|
||||
// }
|
||||
@@ -1,340 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class SeriesSizeItem {
|
||||
constructor() {
|
||||
this.scrollIndex = -1;
|
||||
this.frozenIndex = -1;
|
||||
this.modelIndex = 0;
|
||||
this.size = 0;
|
||||
this.position = 0;
|
||||
}
|
||||
|
||||
// modelIndex;
|
||||
// size;
|
||||
// position;
|
||||
|
||||
get endPosition() {
|
||||
return this.position + this.size;
|
||||
}
|
||||
}
|
||||
|
||||
export class SeriesSizes {
|
||||
constructor() {
|
||||
this.scrollItems = [];
|
||||
this.sizeOverridesByModelIndex = {};
|
||||
this.positions = [];
|
||||
this.scrollIndexes = [];
|
||||
this.frozenItems = [];
|
||||
this.hiddenAndFrozenModelIndexes = null;
|
||||
this.frozenModelIndexes = null;
|
||||
|
||||
this.count = 0;
|
||||
this.maxSize = 1000;
|
||||
this.defaultSize = 50;
|
||||
}
|
||||
|
||||
// private sizeOverridesByModelIndex: { [id] } = {};
|
||||
// count;
|
||||
// defaultSize;
|
||||
// maxSize;
|
||||
// private hiddenAndFrozenModelIndexes[] = [];
|
||||
// private frozenModelIndexes[] = [];
|
||||
// private hiddenModelIndexes[] = [];
|
||||
// private scrollItems: SeriesSizeItem[] = [];
|
||||
// private positions[] = [];
|
||||
// private scrollIndexes[] = [];
|
||||
// private frozenItems: SeriesSizeItem[] = [];
|
||||
|
||||
get scrollCount() {
|
||||
return this.count - (this.hiddenAndFrozenModelIndexes != null ? this.hiddenAndFrozenModelIndexes.length : 0);
|
||||
}
|
||||
get frozenCount() {
|
||||
return this.frozenModelIndexes != null ? this.frozenModelIndexes.length : 0;
|
||||
}
|
||||
get frozenSize() {
|
||||
return _.sumBy(this.frozenItems, x => x.size);
|
||||
}
|
||||
get realCount() {
|
||||
return this.frozenCount + this.scrollCount;
|
||||
}
|
||||
|
||||
putSizeOverride(modelIndex, size, sizeByUser = false) {
|
||||
if (this.maxSize && size > this.maxSize && !sizeByUser) {
|
||||
size = this.maxSize;
|
||||
}
|
||||
|
||||
let currentSize = this.sizeOverridesByModelIndex[modelIndex];
|
||||
if (sizeByUser || !currentSize || size > currentSize) {
|
||||
this.sizeOverridesByModelIndex[modelIndex] = size;
|
||||
}
|
||||
// if (!_.has(this.sizeOverridesByModelIndex, modelIndex))
|
||||
// this.sizeOverridesByModelIndex[modelIndex] = size;
|
||||
// if (size > this.sizeOverridesByModelIndex[modelIndex])
|
||||
// this.sizeOverridesByModelIndex[modelIndex] = size;
|
||||
}
|
||||
buildIndex() {
|
||||
this.scrollItems = [];
|
||||
this.scrollIndexes = _.filter(
|
||||
_.map(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelToReal(x) - this.frozenCount),
|
||||
x => x >= 0
|
||||
);
|
||||
this.scrollIndexes.sort();
|
||||
let lastScrollIndex = -1;
|
||||
let lastEndPosition = 0;
|
||||
this.scrollIndexes.forEach(scrollIndex => {
|
||||
let modelIndex = this.realToModel(scrollIndex + this.frozenCount);
|
||||
let size = this.sizeOverridesByModelIndex[modelIndex];
|
||||
let item = new SeriesSizeItem();
|
||||
item.scrollIndex = scrollIndex;
|
||||
item.modelIndex = modelIndex;
|
||||
item.size = size;
|
||||
item.position = lastEndPosition + (scrollIndex - lastScrollIndex - 1) * this.defaultSize;
|
||||
this.scrollItems.push(item);
|
||||
lastScrollIndex = scrollIndex;
|
||||
lastEndPosition = item.endPosition;
|
||||
});
|
||||
this.positions = _.map(this.scrollItems, x => x.position);
|
||||
this.frozenItems = [];
|
||||
let lastpos = 0;
|
||||
for (let i = 0; i < this.frozenCount; i++) {
|
||||
let modelIndex = this.frozenModelIndexes[i];
|
||||
let size = this.getSizeByModelIndex(modelIndex);
|
||||
let item = new SeriesSizeItem();
|
||||
item.frozenIndex = i;
|
||||
item.modelIndex = modelIndex;
|
||||
item.size = size;
|
||||
item.position = lastpos;
|
||||
this.frozenItems.push(item);
|
||||
lastpos += size;
|
||||
}
|
||||
}
|
||||
|
||||
getScrollIndexOnPosition(position) {
|
||||
let itemOrder = _.sortedIndex(this.positions, position);
|
||||
if (this.positions[itemOrder] == position) return itemOrder;
|
||||
if (itemOrder == 0) return Math.floor(position / this.defaultSize);
|
||||
if (position <= this.scrollItems[itemOrder - 1].endPosition) return this.scrollItems[itemOrder - 1].scrollIndex;
|
||||
return (
|
||||
Math.floor((position - this.scrollItems[itemOrder - 1].position) / this.defaultSize) +
|
||||
this.scrollItems[itemOrder - 1].scrollIndex
|
||||
);
|
||||
}
|
||||
getFrozenIndexOnPosition(position) {
|
||||
this.frozenItems.forEach(function (item) {
|
||||
if (position >= item.position && position <= item.endPosition) return item.frozenIndex;
|
||||
});
|
||||
return -1;
|
||||
}
|
||||
// getSizeSum(startScrollIndex, endScrollIndex) {
|
||||
// let order1 = _.sortedIndexOf(this.scrollIndexes, startScrollIndex);
|
||||
// let order2 = _.sortedIndexOf(this.scrollIndexes, endScrollIndex);
|
||||
// let count = endScrollIndex - startScrollIndex;
|
||||
// if (order1 < 0)
|
||||
// order1 = ~order1;
|
||||
// if (order2 < 0)
|
||||
// order2 = ~order2;
|
||||
// let result = 0;
|
||||
// for (let i = order1; i <= order2; i++) {
|
||||
// if (i < 0)
|
||||
// continue;
|
||||
// if (i >= this.scrollItems.length)
|
||||
// continue;
|
||||
// let item = this.scrollItems[i];
|
||||
// if (item.scrollIndex < startScrollIndex)
|
||||
// continue;
|
||||
// if (item.scrollIndex >= endScrollIndex)
|
||||
// continue;
|
||||
// result += item.size;
|
||||
// count--;
|
||||
// }
|
||||
// result += count * this.defaultSize;
|
||||
// return result;
|
||||
// }
|
||||
getSizeByModelIndex(modelIndex) {
|
||||
if (_.has(this.sizeOverridesByModelIndex, modelIndex)) return this.sizeOverridesByModelIndex[modelIndex];
|
||||
return this.defaultSize;
|
||||
}
|
||||
getSizeByScrollIndex(scrollIndex) {
|
||||
return this.getSizeByRealIndex(scrollIndex + this.frozenCount);
|
||||
}
|
||||
getSizeByRealIndex(realIndex) {
|
||||
let modelIndex = this.realToModel(realIndex);
|
||||
return this.getSizeByModelIndex(modelIndex);
|
||||
}
|
||||
removeSizeOverride(realIndex) {
|
||||
let modelIndex = this.realToModel(realIndex);
|
||||
delete this.sizeOverridesByModelIndex[modelIndex];
|
||||
}
|
||||
getScroll(sourceScrollIndex, targetScrollIndex) {
|
||||
if (sourceScrollIndex < targetScrollIndex) {
|
||||
return -_.sum(
|
||||
_.map(_.range(sourceScrollIndex, targetScrollIndex - sourceScrollIndex), x => this.getSizeByScrollIndex(x))
|
||||
);
|
||||
} else {
|
||||
return _.sum(
|
||||
_.map(_.range(targetScrollIndex, sourceScrollIndex - targetScrollIndex), x => this.getSizeByScrollIndex(x))
|
||||
);
|
||||
}
|
||||
}
|
||||
modelIndexIsInScrollArea(modelIndex) {
|
||||
let realIndex = this.modelToReal(modelIndex);
|
||||
return realIndex >= this.frozenCount;
|
||||
}
|
||||
getTotalScrollSizeSum() {
|
||||
let scrollSizeOverrides = _.map(
|
||||
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelIndexIsInScrollArea(x)),
|
||||
x => this.sizeOverridesByModelIndex[x]
|
||||
);
|
||||
return _.sum(scrollSizeOverrides) + (this.count - scrollSizeOverrides.length) * this.defaultSize;
|
||||
}
|
||||
getVisibleScrollSizeSum() {
|
||||
let scrollSizeOverrides = _.map(
|
||||
_.filter(this.intKeys(this.sizeOverridesByModelIndex), x => !_.includes(this.hiddenAndFrozenModelIndexes, x)),
|
||||
x => this.sizeOverridesByModelIndex[x]
|
||||
);
|
||||
return (
|
||||
_.sum(scrollSizeOverrides) +
|
||||
(this.count - this.hiddenModelIndexes.length - scrollSizeOverrides.length) * this.defaultSize
|
||||
);
|
||||
}
|
||||
intKeys(value) {
|
||||
return _.keys(value).map(x => _.parseInt(x));
|
||||
}
|
||||
getPositionByRealIndex(realIndex) {
|
||||
if (realIndex < 0) return 0;
|
||||
if (realIndex < this.frozenCount) return this.frozenItems[realIndex].position;
|
||||
return this.getPositionByScrollIndex(realIndex - this.frozenCount);
|
||||
}
|
||||
getPositionByScrollIndex(scrollIndex) {
|
||||
let order = _.sortedIndex(this.scrollIndexes, scrollIndex);
|
||||
if (this.scrollIndexes[order] == scrollIndex) return this.scrollItems[order].position;
|
||||
order--;
|
||||
if (order < 0) return scrollIndex * this.defaultSize;
|
||||
return (
|
||||
this.scrollItems[order].endPosition + (scrollIndex - this.scrollItems[order].scrollIndex - 1) * this.defaultSize
|
||||
);
|
||||
}
|
||||
getVisibleScrollCount(firstVisibleIndex, viewportSize) {
|
||||
let res = 0;
|
||||
let index = firstVisibleIndex;
|
||||
let count = 0;
|
||||
while (res < viewportSize && index <= this.scrollCount) {
|
||||
// console.log('this.getSizeByScrollIndex(index)', this.getSizeByScrollIndex(index));
|
||||
res += this.getSizeByScrollIndex(index);
|
||||
index++;
|
||||
count++;
|
||||
}
|
||||
// console.log('getVisibleScrollCount', firstVisibleIndex, viewportSize, count);
|
||||
return count;
|
||||
}
|
||||
getVisibleScrollCountReversed(lastVisibleIndex, viewportSize) {
|
||||
let res = 0;
|
||||
let index = lastVisibleIndex;
|
||||
let count = 0;
|
||||
while (res < viewportSize && index >= 0) {
|
||||
res += this.getSizeByScrollIndex(index);
|
||||
index--;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
invalidateAfterScroll(oldFirstVisible, newFirstVisible, invalidate, viewportSize) {
|
||||
if (newFirstVisible > oldFirstVisible) {
|
||||
let oldVisibleCount = this.getVisibleScrollCount(oldFirstVisible, viewportSize);
|
||||
let newVisibleCount = this.getVisibleScrollCount(newFirstVisible, viewportSize);
|
||||
for (let i = oldFirstVisible + oldVisibleCount - 1; i <= newFirstVisible + newVisibleCount; i++) {
|
||||
invalidate(i + this.frozenCount);
|
||||
}
|
||||
} else {
|
||||
for (let i = newFirstVisible; i <= oldFirstVisible; i++) {
|
||||
invalidate(i + this.frozenCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
isWholeInView(firstVisibleIndex, index, viewportSize) {
|
||||
let res = 0;
|
||||
let testedIndex = firstVisibleIndex;
|
||||
while (res < viewportSize && testedIndex < this.count) {
|
||||
res += this.getSizeByScrollIndex(testedIndex);
|
||||
if (testedIndex == index) return res <= viewportSize;
|
||||
testedIndex++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
scrollInView(firstVisibleIndex, scrollIndex, viewportSize) {
|
||||
if (this.isWholeInView(firstVisibleIndex, scrollIndex, viewportSize)) {
|
||||
return firstVisibleIndex;
|
||||
}
|
||||
if (scrollIndex < firstVisibleIndex) {
|
||||
return scrollIndex;
|
||||
}
|
||||
let res = 0;
|
||||
let testedIndex = scrollIndex;
|
||||
while (res < viewportSize && testedIndex >= 0) {
|
||||
let size = this.getSizeByScrollIndex(testedIndex);
|
||||
if (res + size > viewportSize) return testedIndex + 1;
|
||||
testedIndex--;
|
||||
res += size;
|
||||
}
|
||||
if (res >= viewportSize && testedIndex < scrollIndex) return testedIndex + 1;
|
||||
return firstVisibleIndex;
|
||||
}
|
||||
resize(realIndex, newSize) {
|
||||
if (realIndex < 0) return;
|
||||
let modelIndex = this.realToModel(realIndex);
|
||||
if (modelIndex < 0) return;
|
||||
this.sizeOverridesByModelIndex[modelIndex] = newSize;
|
||||
this.buildIndex();
|
||||
}
|
||||
setExtraordinaryIndexes(hidden, frozen) {
|
||||
//this._hiddenAndFrozenModelIndexes = _.clone(hidden);
|
||||
hidden = hidden.filter(x => x >= 0);
|
||||
frozen = frozen.filter(x => x >= 0);
|
||||
|
||||
hidden.sort((a, b) => a - b);
|
||||
frozen.sort((a, b) => a - b);
|
||||
this.frozenModelIndexes = _.filter(frozen, x => !_.includes(hidden, x));
|
||||
this.hiddenModelIndexes = _.filter(hidden, x => !_.includes(frozen, x));
|
||||
this.hiddenAndFrozenModelIndexes = _.concat(hidden, this.frozenModelIndexes);
|
||||
this.frozenModelIndexes.sort((a, b) => a - b);
|
||||
if (this.hiddenAndFrozenModelIndexes.length == 0) this.hiddenAndFrozenModelIndexes = null;
|
||||
if (this.frozenModelIndexes.length == 0) this.frozenModelIndexes = null;
|
||||
this.buildIndex();
|
||||
}
|
||||
realToModel(realIndex) {
|
||||
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return realIndex;
|
||||
if (realIndex < 0) return -1;
|
||||
if (realIndex < this.frozenCount && this.frozenModelIndexes != null) return this.frozenModelIndexes[realIndex];
|
||||
if (this.hiddenAndFrozenModelIndexes == null) return realIndex;
|
||||
realIndex -= this.frozenCount;
|
||||
for (let hidItem of this.hiddenAndFrozenModelIndexes) {
|
||||
if (realIndex < hidItem) return realIndex;
|
||||
realIndex++;
|
||||
}
|
||||
return realIndex;
|
||||
}
|
||||
modelToReal(modelIndex) {
|
||||
if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return modelIndex;
|
||||
if (modelIndex < 0) return -1;
|
||||
let frozenIndex = this.frozenModelIndexes != null ? _.indexOf(this.frozenModelIndexes, modelIndex) : -1;
|
||||
if (frozenIndex >= 0) return frozenIndex;
|
||||
if (this.hiddenAndFrozenModelIndexes == null) return modelIndex;
|
||||
let hiddenIndex = _.sortedIndex(this.hiddenAndFrozenModelIndexes, modelIndex);
|
||||
if (this.hiddenAndFrozenModelIndexes[hiddenIndex] == modelIndex) return -1;
|
||||
if (hiddenIndex >= 0) return modelIndex - hiddenIndex + this.frozenCount;
|
||||
return modelIndex;
|
||||
}
|
||||
getFrozenPosition(frozenIndex) {
|
||||
return this.frozenItems[frozenIndex].position;
|
||||
}
|
||||
hasSizeOverride(modelIndex) {
|
||||
return _.has(this.sizeOverridesByModelIndex, modelIndex);
|
||||
}
|
||||
isVisible(testedRealIndex, firstVisibleScrollIndex, viewportSize) {
|
||||
if (testedRealIndex < 0) return false;
|
||||
if (testedRealIndex >= 0 && testedRealIndex < this.frozenCount) return true;
|
||||
let scrollIndex = testedRealIndex - this.frozenCount;
|
||||
let onPageIndex = scrollIndex - firstVisibleScrollIndex;
|
||||
return onPageIndex >= 0 && onPageIndex < this.getVisibleScrollCount(firstVisibleScrollIndex, viewportSize);
|
||||
}
|
||||
}
|
||||
@@ -1,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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
65
packages/web/src/datagrid/SqlDataGridCore.svelte
Normal file
65
packages/web/src/datagrid/SqlDataGridCore.svelte
Normal 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} />
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
47
packages/web/src/datagrid/TableDataGrid.svelte
Normal file
47
packages/web/src/datagrid/TableDataGrid.svelte
Normal 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} />
|
||||
42
packages/web/src/datagrid/VerticalScrollBar.svelte
Normal file
42
packages/web/src/datagrid/VerticalScrollBar.svelte
Normal 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`}> </div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
overflow-y: scroll;
|
||||
width: 20px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 20px;
|
||||
bottom: 16px;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import DesignerTable from './DesignerTable';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import _ from 'lodash';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import DesignerReference from './DesignerReference';
|
||||
import cleanupDesignColumns from './cleanupDesignColumns';
|
||||
import { isConnectedByReference } from './designerTools';
|
||||
import { getTableInfo } from '../utility/metadataLoaders';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
flex: 1;
|
||||
background-color: ${props => props.theme.designer_background};
|
||||
overflow: scroll;
|
||||
`;
|
||||
|
||||
const Canvas = styled.div`
|
||||
width: 3000px;
|
||||
height: 3000px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const EmptyInfo = styled.div`
|
||||
margin: 50px;
|
||||
font-size: 20px;
|
||||
`;
|
||||
|
||||
function fixPositions(tables) {
|
||||
const minLeft = _.min(tables.map(x => x.left));
|
||||
const minTop = _.min(tables.map(x => x.top));
|
||||
if (minLeft < 0 || minTop < 0) {
|
||||
const dLeft = minLeft < 0 ? -minLeft : 0;
|
||||
const dTop = minTop < 0 ? -minTop : 0;
|
||||
return tables.map(tbl => ({
|
||||
...tbl,
|
||||
left: tbl.left + dLeft,
|
||||
top: tbl.top + dTop,
|
||||
}));
|
||||
}
|
||||
return tables;
|
||||
}
|
||||
|
||||
export default function Designer({ value, onChange, conid, database }) {
|
||||
const { tables, references } = value || {};
|
||||
const theme = useTheme();
|
||||
|
||||
const [sourceDragColumn, setSourceDragColumn] = React.useState(null);
|
||||
const [targetDragColumn, setTargetDragColumn] = React.useState(null);
|
||||
const domTablesRef = React.useRef({});
|
||||
const wrapperRef = React.useRef();
|
||||
const [changeToken, setChangeToken] = React.useState(0);
|
||||
|
||||
const handleDrop = e => {
|
||||
var data = e.dataTransfer.getData('app_object_drag_data');
|
||||
e.preventDefault();
|
||||
if (!data) return;
|
||||
const rect = e.target.getBoundingClientRect();
|
||||
var json = JSON.parse(data);
|
||||
const { objectTypeField } = json;
|
||||
if (objectTypeField != 'tables' && objectTypeField != 'views') return;
|
||||
json.designerId = uuidv1();
|
||||
json.left = e.clientX - rect.left;
|
||||
json.top = e.clientY - rect.top;
|
||||
|
||||
onChange(current => {
|
||||
const foreignKeys = _.compact([
|
||||
...(json.foreignKeys || []).map(fk => {
|
||||
const tables = ((current || {}).tables || []).filter(
|
||||
tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName
|
||||
);
|
||||
if (tables.length == 1)
|
||||
return {
|
||||
...fk,
|
||||
sourceId: json.designerId,
|
||||
targetId: tables[0].designerId,
|
||||
};
|
||||
return null;
|
||||
}),
|
||||
..._.flatten(
|
||||
((current || {}).tables || []).map(tbl =>
|
||||
(tbl.foreignKeys || []).map(fk => {
|
||||
if (fk.refTableName == json.pureName && fk.refSchemaName == json.schemaName) {
|
||||
return {
|
||||
...fk,
|
||||
sourceId: tbl.designerId,
|
||||
targetId: json.designerId,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
...current,
|
||||
tables: [...((current || {}).tables || []), json],
|
||||
references:
|
||||
foreignKeys.length == 1
|
||||
? [
|
||||
...((current || {}).references || []),
|
||||
{
|
||||
designerId: uuidv1(),
|
||||
sourceId: foreignKeys[0].sourceId,
|
||||
targetId: foreignKeys[0].targetId,
|
||||
joinType: 'INNER JOIN',
|
||||
columns: foreignKeys[0].columns.map(col => ({
|
||||
source: col.columnName,
|
||||
target: col.refColumnName,
|
||||
})),
|
||||
},
|
||||
]
|
||||
: (current || {}).references,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const changeTable = React.useCallback(
|
||||
table => {
|
||||
onChange(current => ({
|
||||
...current,
|
||||
tables: fixPositions((current.tables || []).map(x => (x.designerId == table.designerId ? table : x))),
|
||||
}));
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const bringToFront = React.useCallback(
|
||||
table => {
|
||||
onChange(
|
||||
current => ({
|
||||
...current,
|
||||
tables: [...(current.tables || []).filter(x => x.designerId != table.designerId), table],
|
||||
}),
|
||||
true
|
||||
);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const removeTable = React.useCallback(
|
||||
table => {
|
||||
onChange(current => ({
|
||||
...current,
|
||||
tables: (current.tables || []).filter(x => x.designerId != table.designerId),
|
||||
references: (current.references || []).filter(
|
||||
x => x.sourceId != table.designerId && x.targetId != table.designerId
|
||||
),
|
||||
columns: (current.columns || []).filter(x => x.designerId != table.designerId),
|
||||
}));
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const changeReference = React.useCallback(
|
||||
ref => {
|
||||
onChange(current => ({
|
||||
...current,
|
||||
references: (current.references || []).map(x => (x.designerId == ref.designerId ? ref : x)),
|
||||
}));
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const removeReference = React.useCallback(
|
||||
ref => {
|
||||
onChange(current => ({
|
||||
...current,
|
||||
references: (current.references || []).filter(x => x.designerId != ref.designerId),
|
||||
}));
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleCreateReference = (source, target) => {
|
||||
onChange(current => {
|
||||
const existingReference = (current.references || []).find(
|
||||
x =>
|
||||
(x.sourceId == source.designerId && x.targetId == target.designerId) ||
|
||||
(x.sourceId == target.designerId && x.targetId == source.designerId)
|
||||
);
|
||||
|
||||
return {
|
||||
...current,
|
||||
references: existingReference
|
||||
? current.references.map(ref =>
|
||||
ref == existingReference
|
||||
? {
|
||||
...existingReference,
|
||||
columns: [
|
||||
...existingReference.columns,
|
||||
existingReference.sourceId == source.designerId
|
||||
? {
|
||||
source: source.columnName,
|
||||
target: target.columnName,
|
||||
}
|
||||
: {
|
||||
source: target.columnName,
|
||||
target: source.columnName,
|
||||
},
|
||||
],
|
||||
}
|
||||
: ref
|
||||
)
|
||||
: [
|
||||
...(current.references || []),
|
||||
{
|
||||
designerId: uuidv1(),
|
||||
sourceId: source.designerId,
|
||||
targetId: target.designerId,
|
||||
joinType: isConnectedByReference(current, source, target, null) ? 'CROSS JOIN' : 'INNER JOIN',
|
||||
columns: [
|
||||
{
|
||||
source: source.columnName,
|
||||
target: target.columnName,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddReferenceByColumn = async (designerId, foreignKey) => {
|
||||
const toTable = await getTableInfo({
|
||||
conid,
|
||||
database,
|
||||
pureName: foreignKey.refTableName,
|
||||
schemaName: foreignKey.refSchemaName,
|
||||
});
|
||||
const newTableDesignerId = uuidv1();
|
||||
onChange(current => {
|
||||
const fromTable = (current.tables || []).find(x => x.designerId == designerId);
|
||||
if (!fromTable) return;
|
||||
return {
|
||||
...current,
|
||||
tables: [
|
||||
...(current.tables || []),
|
||||
{
|
||||
...toTable,
|
||||
left: fromTable.left + 300,
|
||||
top: fromTable.top + 50,
|
||||
designerId: newTableDesignerId,
|
||||
},
|
||||
],
|
||||
references: [
|
||||
...(current.references || []),
|
||||
{
|
||||
designerId: uuidv1(),
|
||||
sourceId: fromTable.designerId,
|
||||
targetId: newTableDesignerId,
|
||||
joinType: 'INNER JOIN',
|
||||
columns: foreignKey.columns.map(col => ({
|
||||
source: col.columnName,
|
||||
target: col.refColumnName,
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectColumn = React.useCallback(
|
||||
column => {
|
||||
onChange(
|
||||
current => ({
|
||||
...current,
|
||||
columns: (current.columns || []).find(
|
||||
x => x.designerId == column.designerId && x.columnName == column.columnName
|
||||
)
|
||||
? current.columns
|
||||
: [...cleanupDesignColumns(current.columns), _.pick(column, ['designerId', 'columnName'])],
|
||||
}),
|
||||
true
|
||||
);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleChangeColumn = React.useCallback(
|
||||
(column, changeFunc) => {
|
||||
onChange(current => {
|
||||
const currentColumns = (current || {}).columns || [];
|
||||
const existing = currentColumns.find(
|
||||
x => x.designerId == column.designerId && x.columnName == column.columnName
|
||||
);
|
||||
if (existing) {
|
||||
return {
|
||||
...current,
|
||||
columns: currentColumns.map(x => (x == existing ? changeFunc(existing) : x)),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...current,
|
||||
columns: [
|
||||
...cleanupDesignColumns(currentColumns),
|
||||
changeFunc(_.pick(column, ['designerId', 'columnName'])),
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
// React.useEffect(() => {
|
||||
// setTimeout(() => setChangeToken((x) => x + 1), 100);
|
||||
// }, [value]);
|
||||
|
||||
return (
|
||||
<Wrapper theme={theme}>
|
||||
{(tables || []).length == 0 && <EmptyInfo>Drag & drop tables or views from left panel here</EmptyInfo>}
|
||||
<Canvas onDragOver={e => e.preventDefault()} onDrop={handleDrop} ref={wrapperRef}>
|
||||
{(references || []).map(ref => (
|
||||
<DesignerReference
|
||||
key={ref.designerId}
|
||||
changeToken={changeToken}
|
||||
domTablesRef={domTablesRef}
|
||||
reference={ref}
|
||||
onChangeReference={changeReference}
|
||||
onRemoveReference={removeReference}
|
||||
designer={value}
|
||||
/>
|
||||
))}
|
||||
{(tables || []).map(table => (
|
||||
<DesignerTable
|
||||
key={table.designerId}
|
||||
sourceDragColumn={sourceDragColumn}
|
||||
setSourceDragColumn={setSourceDragColumn}
|
||||
targetDragColumn={targetDragColumn}
|
||||
setTargetDragColumn={setTargetDragColumn}
|
||||
onCreateReference={handleCreateReference}
|
||||
onSelectColumn={handleSelectColumn}
|
||||
onChangeColumn={handleChangeColumn}
|
||||
onAddReferenceByColumn={handleAddReferenceByColumn}
|
||||
table={table}
|
||||
onChangeTable={changeTable}
|
||||
onBringToFront={bringToFront}
|
||||
onRemoveTable={removeTable}
|
||||
setChangeToken={setChangeToken}
|
||||
wrapperRef={wrapperRef}
|
||||
onChangeDomTable={table => {
|
||||
domTablesRef.current[table.designerId] = table;
|
||||
}}
|
||||
designer={value}
|
||||
/>
|
||||
))}
|
||||
</Canvas>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import { dumpSqlSelect, Select, JoinType, Condition, Relation, mergeConditions, Source } from 'dbgate-sqltree';
|
||||
import { EngineDriver } from 'dbgate-types';
|
||||
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
|
||||
import { findPrimaryTable, findConnectingReference, referenceIsJoin, referenceIsExists } from './designerTools';
|
||||
|
||||
export class DesignerComponent {
|
||||
subComponents: DesignerComponent[] = [];
|
||||
parentComponent: DesignerComponent;
|
||||
parentReference: DesignerReferenceInfo;
|
||||
|
||||
tables: DesignerTableInfo[] = [];
|
||||
nonPrimaryReferences: DesignerReferenceInfo[] = [];
|
||||
|
||||
get primaryTable() {
|
||||
return this.tables[0];
|
||||
}
|
||||
get nonPrimaryTables() {
|
||||
return this.tables.slice(1);
|
||||
}
|
||||
get nonPrimaryTablesAndReferences() {
|
||||
return _.zip(this.nonPrimaryTables, this.nonPrimaryReferences);
|
||||
}
|
||||
get myAndParentTables() {
|
||||
return [...this.parentTables, ...this.tables];
|
||||
}
|
||||
get parentTables() {
|
||||
return this.parentComponent ? this.parentComponent.myAndParentTables : [];
|
||||
}
|
||||
get thisAndSubComponentsTables() {
|
||||
return [...this.tables, ..._.flatten(this.subComponents.map(x => x.thisAndSubComponentsTables))];
|
||||
}
|
||||
}
|
||||
|
||||
export class DesignerComponentCreator {
|
||||
toAdd: DesignerTableInfo[];
|
||||
components: DesignerComponent[] = [];
|
||||
|
||||
constructor(public designer: DesignerInfo) {
|
||||
this.toAdd = [...designer.tables];
|
||||
while (this.toAdd.length > 0) {
|
||||
const component = this.parseComponent(null);
|
||||
this.components.push(component);
|
||||
}
|
||||
}
|
||||
|
||||
parseComponent(root) {
|
||||
if (root == null) {
|
||||
root = findPrimaryTable(this.toAdd);
|
||||
}
|
||||
if (!root) return null;
|
||||
_.remove(this.toAdd, x => x == root);
|
||||
const res = new DesignerComponent();
|
||||
res.tables.push(root);
|
||||
|
||||
for (;;) {
|
||||
let found = false;
|
||||
for (const test of this.toAdd) {
|
||||
const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsJoin);
|
||||
if (ref) {
|
||||
res.tables.push(test);
|
||||
res.nonPrimaryReferences.push(ref);
|
||||
_.remove(this.toAdd, x => x == test);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) break;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
let found = false;
|
||||
for (const test of this.toAdd) {
|
||||
const ref = findConnectingReference(this.designer, res.tables, [test], referenceIsExists);
|
||||
if (ref) {
|
||||
const subComponent = this.parseComponent(test);
|
||||
res.subComponents.push(subComponent);
|
||||
subComponent.parentComponent = res;
|
||||
subComponent.parentReference = ref;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
dumpSqlSelect,
|
||||
Select,
|
||||
JoinType,
|
||||
Condition,
|
||||
Relation,
|
||||
mergeConditions,
|
||||
Source,
|
||||
ResultField,
|
||||
} from 'dbgate-sqltree';
|
||||
import { EngineDriver } from 'dbgate-types';
|
||||
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
|
||||
import { DesignerComponent } from './DesignerComponentCreator';
|
||||
import {
|
||||
getReferenceConditions,
|
||||
referenceIsCrossJoin,
|
||||
referenceIsConnecting,
|
||||
mergeSelectsFromDesigner,
|
||||
findQuerySource,
|
||||
findDesignerFilterType,
|
||||
} from './designerTools';
|
||||
import { parseFilter } from 'dbgate-filterparser';
|
||||
|
||||
export class DesignerQueryDumper {
|
||||
constructor(public designer: DesignerInfo, public components: DesignerComponent[]) {}
|
||||
|
||||
get topLevelTables(): DesignerTableInfo[] {
|
||||
return _.flatten(this.components.map(x => x.tables));
|
||||
}
|
||||
|
||||
dumpComponent(component: DesignerComponent) {
|
||||
const select: Select = {
|
||||
commandType: 'select',
|
||||
from: {
|
||||
name: component.primaryTable,
|
||||
alias: component.primaryTable.alias,
|
||||
relations: [],
|
||||
},
|
||||
};
|
||||
|
||||
for (const [table, ref] of component.nonPrimaryTablesAndReferences) {
|
||||
select.from.relations.push({
|
||||
name: table,
|
||||
alias: table.alias,
|
||||
joinType: ref.joinType as JoinType,
|
||||
conditions: getReferenceConditions(ref, this.designer),
|
||||
});
|
||||
}
|
||||
|
||||
for (const subComponent of component.subComponents) {
|
||||
const subQuery = this.dumpComponent(subComponent);
|
||||
subQuery.selectAll = true;
|
||||
select.where = mergeConditions(select.where, {
|
||||
conditionType: subComponent.parentReference.joinType == 'WHERE NOT EXISTS' ? 'notExists' : 'exists',
|
||||
subQuery,
|
||||
});
|
||||
}
|
||||
|
||||
if (component.parentReference) {
|
||||
select.where = mergeConditions(select.where, {
|
||||
conditionType: 'and',
|
||||
conditions: getReferenceConditions(component.parentReference, this.designer),
|
||||
});
|
||||
|
||||
// cross join conditions in subcomponents
|
||||
for (const ref of this.designer.references || []) {
|
||||
if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, component.tables, component.myAndParentTables)) {
|
||||
select.where = mergeConditions(select.where, {
|
||||
conditionType: 'and',
|
||||
conditions: getReferenceConditions(ref, this.designer),
|
||||
});
|
||||
}
|
||||
}
|
||||
this.addConditions(select, component.tables);
|
||||
}
|
||||
|
||||
return select;
|
||||
}
|
||||
|
||||
addConditions(select: Select, tables: DesignerTableInfo[]) {
|
||||
for (const column of this.designer.columns || []) {
|
||||
if (!column.filter) continue;
|
||||
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
|
||||
if (!table) continue;
|
||||
if (!tables.find(x => x.designerId == table.designerId)) continue;
|
||||
|
||||
const condition = parseFilter(column.filter, findDesignerFilterType(column, this.designer));
|
||||
if (condition) {
|
||||
select.where = mergeConditions(
|
||||
select.where,
|
||||
_.cloneDeepWith(condition, expr => {
|
||||
if (expr.exprType == 'placeholder')
|
||||
return {
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
source: findQuerySource(this.designer, column.designerId),
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addGroupConditions(select: Select, tables: DesignerTableInfo[], selectIsGrouped: boolean) {
|
||||
for (const column of this.designer.columns || []) {
|
||||
if (!column.groupFilter) continue;
|
||||
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
|
||||
if (!table) continue;
|
||||
if (!tables.find(x => x.designerId == table.designerId)) continue;
|
||||
|
||||
const condition = parseFilter(column.groupFilter, findDesignerFilterType(column, this.designer));
|
||||
if (condition) {
|
||||
select.having = mergeConditions(
|
||||
select.having,
|
||||
_.cloneDeepWith(condition, expr => {
|
||||
if (expr.exprType == 'placeholder') {
|
||||
return this.getColumnOutputExpression(column, selectIsGrouped);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getColumnOutputExpression(col, selectIsGrouped): ResultField {
|
||||
const source = findQuerySource(this.designer, col.designerId);
|
||||
const { columnName } = col;
|
||||
let { alias } = col;
|
||||
if (selectIsGrouped && !col.isGrouped) {
|
||||
// use aggregate
|
||||
const aggregate = col.aggregate == null || col.aggregate == '---' ? 'MAX' : col.aggregate;
|
||||
if (!alias) alias = `${aggregate}(${columnName})`;
|
||||
|
||||
return {
|
||||
exprType: 'call',
|
||||
func: aggregate == 'COUNT DISTINCT' ? 'COUNT' : aggregate,
|
||||
argsPrefix: aggregate == 'COUNT DISTINCT' ? 'DISTINCT' : null,
|
||||
alias,
|
||||
args: [
|
||||
{
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
alias,
|
||||
source,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
run() {
|
||||
let res: Select = null;
|
||||
for (const component of this.components) {
|
||||
const select = this.dumpComponent(component);
|
||||
if (res == null) res = select;
|
||||
else res = mergeSelectsFromDesigner(res, select);
|
||||
}
|
||||
|
||||
// top level cross join conditions
|
||||
const topLevelTables = this.topLevelTables;
|
||||
for (const ref of this.designer.references || []) {
|
||||
if (referenceIsCrossJoin(ref) && referenceIsConnecting(ref, topLevelTables, topLevelTables)) {
|
||||
res.where = mergeConditions(res.where, {
|
||||
conditionType: 'and',
|
||||
conditions: getReferenceConditions(ref, this.designer),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const topLevelColumns = (this.designer.columns || []).filter(col =>
|
||||
topLevelTables.find(tbl => tbl.designerId == col.designerId)
|
||||
);
|
||||
const selectIsGrouped = !!topLevelColumns.find(x => x.isGrouped || (x.aggregate && x.aggregate != '---'));
|
||||
const outputColumns = topLevelColumns.filter(x => x.isOutput);
|
||||
if (outputColumns.length == 0) {
|
||||
res.selectAll = true;
|
||||
} else {
|
||||
res.columns = outputColumns.map(col => this.getColumnOutputExpression(col, selectIsGrouped));
|
||||
}
|
||||
|
||||
const groupedColumns = topLevelColumns.filter(x => x.isGrouped);
|
||||
if (groupedColumns.length > 0) {
|
||||
res.groupBy = groupedColumns.map(col => ({
|
||||
exprType: 'column',
|
||||
columnName: col.columnName,
|
||||
source: findQuerySource(this.designer, col.designerId),
|
||||
}));
|
||||
}
|
||||
|
||||
const orderColumns = _.sortBy(
|
||||
topLevelColumns.filter(x => x.sortOrder),
|
||||
x => Math.abs(x.sortOrder)
|
||||
);
|
||||
if (orderColumns.length > 0) {
|
||||
res.orderBy = orderColumns.map(col => ({
|
||||
exprType: 'column',
|
||||
direction: col.sortOrder < 0 ? 'DESC' : 'ASC',
|
||||
columnName: col.columnName,
|
||||
source: findQuerySource(this.designer, col.designerId),
|
||||
}));
|
||||
}
|
||||
|
||||
this.addConditions(res, topLevelTables);
|
||||
this.addGroupConditions(res, topLevelTables, selectIsGrouped);
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import DomTableRef from './DomTableRef';
|
||||
import _ from 'lodash';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import { useShowMenu } from '../modals/showMenu';
|
||||
import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { isConnectedByReference } from './designerTools';
|
||||
|
||||
const StyledSvg = styled.svg`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const ReferenceWrapper = styled.div`
|
||||
position: absolute;
|
||||
border: 1px solid ${props => props.theme.designer_line};
|
||||
background-color: ${props => props.theme.designer_background};
|
||||
z-index: 900;
|
||||
border-radius: 10px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
`;
|
||||
|
||||
const ReferenceText = styled.span`
|
||||
position: relative;
|
||||
float: left;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 900;
|
||||
white-space: nowrap;
|
||||
background-color: ${props => props.theme.designer_background};
|
||||
`;
|
||||
|
||||
function ReferenceContextMenu({ remove, setJoinType, isConnected }) {
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={remove}>Remove</DropDownMenuItem>
|
||||
{!isConnected && (
|
||||
<>
|
||||
<DropDownMenuDivider />
|
||||
<DropDownMenuItem onClick={() => setJoinType('INNER JOIN')}>Set INNER JOIN</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setJoinType('LEFT JOIN')}>Set LEFT JOIN</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setJoinType('RIGHT JOIN')}>Set RIGHT JOIN</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setJoinType('FULL OUTER JOIN')}>Set FULL OUTER JOIN</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setJoinType('CROSS JOIN')}>Set CROSS JOIN</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setJoinType('WHERE EXISTS')}>Set WHERE EXISTS</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setJoinType('WHERE NOT EXISTS')}>Set WHERE NOT EXISTS</DropDownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DesignerReference({
|
||||
domTablesRef,
|
||||
reference,
|
||||
changeToken,
|
||||
onRemoveReference,
|
||||
onChangeReference,
|
||||
designer,
|
||||
}) {
|
||||
const { designerId, sourceId, targetId, columns, joinType } = reference;
|
||||
const theme = useTheme();
|
||||
const showMenu = useShowMenu();
|
||||
const domTables = domTablesRef.current;
|
||||
/** @type {DomTableRef} */
|
||||
const sourceTable = domTables[sourceId];
|
||||
/** @type {DomTableRef} */
|
||||
const targetTable = domTables[targetId];
|
||||
if (!sourceTable || !targetTable) return null;
|
||||
const sourceRect = sourceTable.getRect();
|
||||
const targetRect = targetTable.getRect();
|
||||
if (!sourceRect || !targetRect) return null;
|
||||
|
||||
const buswi = 10;
|
||||
const extwi = 25;
|
||||
|
||||
const possibilities = [];
|
||||
possibilities.push({ xsrc: sourceRect.left - buswi, dirsrc: -1, xdst: targetRect.left - buswi, dirdst: -1 });
|
||||
possibilities.push({ xsrc: sourceRect.left - buswi, dirsrc: -1, xdst: targetRect.right + buswi, dirdst: 1 });
|
||||
possibilities.push({ xsrc: sourceRect.right + buswi, dirsrc: 1, xdst: targetRect.left - buswi, dirdst: -1 });
|
||||
possibilities.push({ xsrc: sourceRect.right + buswi, dirsrc: 1, xdst: targetRect.right + buswi, dirdst: 1 });
|
||||
|
||||
let minpos = _.minBy(possibilities, p => Math.abs(p.xsrc - p.xdst));
|
||||
|
||||
let srcY = _.mean(columns.map(x => sourceTable.getColumnY(x.source)));
|
||||
let dstY = _.mean(columns.map(x => targetTable.getColumnY(x.target)));
|
||||
|
||||
if (columns.length == 0) {
|
||||
srcY = sourceTable.getColumnY('');
|
||||
dstY = targetTable.getColumnY('');
|
||||
}
|
||||
|
||||
const src = { x: minpos.xsrc, y: srcY };
|
||||
const dst = { x: minpos.xdst, y: dstY };
|
||||
|
||||
const lineStyle = { fill: 'none', stroke: theme.designer_line, strokeWidth: 2 };
|
||||
|
||||
const handleContextMenu = event => {
|
||||
event.preventDefault();
|
||||
showMenu(
|
||||
event.pageX,
|
||||
event.pageY,
|
||||
<ReferenceContextMenu
|
||||
remove={() => onRemoveReference({ designerId })}
|
||||
isConnected={isConnectedByReference(designer, { designerId: sourceId }, { designerId: targetId }, reference)}
|
||||
setJoinType={joinType => {
|
||||
onChangeReference({
|
||||
...reference,
|
||||
joinType,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledSvg>
|
||||
<polyline
|
||||
points={`
|
||||
${src.x},${src.y}
|
||||
${src.x + extwi * minpos.dirsrc},${src.y}
|
||||
${dst.x + extwi * minpos.dirdst},${dst.y}
|
||||
${dst.x},${dst.y}
|
||||
`}
|
||||
style={lineStyle}
|
||||
/>
|
||||
{columns.map((col, colIndex) => {
|
||||
let y1 = sourceTable.getColumnY(col.source);
|
||||
let y2 = targetTable.getColumnY(col.target);
|
||||
return (
|
||||
<React.Fragment key={colIndex}>
|
||||
<polyline
|
||||
points={`
|
||||
${src.x},${src.y}
|
||||
${src.x},${y1}
|
||||
${src.x - buswi * minpos.dirsrc},${y1}
|
||||
`}
|
||||
style={lineStyle}
|
||||
/>
|
||||
<polyline
|
||||
points={`
|
||||
${dst.x},${dst.y}
|
||||
${dst.x},${y2}
|
||||
${dst.x - buswi * minpos.dirdst},${y2}
|
||||
`}
|
||||
style={lineStyle}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</StyledSvg>
|
||||
<ReferenceWrapper
|
||||
theme={theme}
|
||||
style={{
|
||||
left: (src.x + extwi * minpos.dirsrc + dst.x + extwi * minpos.dirdst) / 2 - 16,
|
||||
top: (src.y + dst.y) / 2 - 16,
|
||||
}}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<ReferenceText theme={theme}>
|
||||
{_.snakeCase(joinType || 'CROSS JOIN')
|
||||
.replace('_', '\xa0')
|
||||
.replace('_', '\xa0')}
|
||||
</ReferenceText>
|
||||
</ReferenceWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { findForeignKeyForColumn } from 'dbgate-tools';
|
||||
import ColumnLabel from '../datagrid/ColumnLabel';
|
||||
import { FontIcon } from '../icons';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import DomTableRef from './DomTableRef';
|
||||
import _ from 'lodash';
|
||||
import { CheckboxField } from '../utility/inputs';
|
||||
import { useShowMenu } from '../modals/showMenu';
|
||||
import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import InputTextModal from '../modals/InputTextModal';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: absolute;
|
||||
// background-color: white;
|
||||
background-color: ${props => props.theme.designtable_background};
|
||||
border: 1px solid ${props => props.theme.border};
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
background: ${props =>
|
||||
// @ts-ignore
|
||||
props.objectTypeField == 'views'
|
||||
? props.theme.designtable_background_magenta[2]
|
||||
: props.theme.designtable_background_blue[2]};
|
||||
border-bottom: 1px solid ${props => props.theme.border};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const ColumnsWrapper = styled.div`
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
width: calc(100% - 10px);
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
const HeaderLabel = styled.div``;
|
||||
|
||||
const CloseWrapper = styled.div`
|
||||
${props =>
|
||||
`
|
||||
background-color: ${props.theme.toolbar_background} ;
|
||||
|
||||
&:hover {
|
||||
background-color: ${props.theme.toolbar_background2} ;
|
||||
}
|
||||
|
||||
&:active:hover {
|
||||
background-color: ${props.theme.toolbar_background3};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
// &:hover {
|
||||
// background-color: ${(props) => props.theme.designtable_background_gold[1]};
|
||||
// }
|
||||
|
||||
const ColumnLine = styled.div`
|
||||
${props =>
|
||||
// @ts-ignore
|
||||
!props.isDragSource &&
|
||||
// @ts-ignore
|
||||
!props.isDragTarget &&
|
||||
`
|
||||
&:hover {
|
||||
background-color: ${props.theme.designtable_background_gold[1]};
|
||||
}
|
||||
`}
|
||||
|
||||
${props =>
|
||||
// @ts-ignore
|
||||
props.isDragSource &&
|
||||
`
|
||||
background-color: ${props.theme.designtable_background_cyan[2]};
|
||||
`}
|
||||
|
||||
${props =>
|
||||
// @ts-ignore
|
||||
props.isDragTarget &&
|
||||
`
|
||||
background-color: ${props.theme.designtable_background_cyan[2]};
|
||||
`}
|
||||
`;
|
||||
|
||||
function TableContextMenu({ remove, setTableAlias, removeTableAlias }) {
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={remove}>Remove</DropDownMenuItem>
|
||||
<DropDownMenuDivider />
|
||||
<DropDownMenuItem onClick={setTableAlias}>Set table alias</DropDownMenuItem>
|
||||
{!!removeTableAlias && <DropDownMenuItem onClick={removeTableAlias}>Remove table alias</DropDownMenuItem>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ColumnContextMenu({ setSortOrder, addReference }) {
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={() => setSortOrder(1)}>Sort ascending</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setSortOrder(-1)}>Sort descending</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setSortOrder(0)}>Unsort</DropDownMenuItem>
|
||||
{!!addReference && <DropDownMenuItem onClick={addReference}>Add reference</DropDownMenuItem>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ColumnDesignerIcons({ column, designerId, designer }) {
|
||||
const designerColumn = (designer.columns || []).find(
|
||||
x => x.designerId == designerId && x.columnName == column.columnName
|
||||
);
|
||||
if (!designerColumn) return null;
|
||||
return (
|
||||
<>
|
||||
{!!designerColumn.filter && <FontIcon icon="img filter" />}
|
||||
{designerColumn.sortOrder > 0 && <FontIcon icon="img sort-asc" />}
|
||||
{designerColumn.sortOrder < 0 && <FontIcon icon="img sort-desc" />}
|
||||
{!!designerColumn.isGrouped && <FontIcon icon="img group" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DesignerTable({
|
||||
table,
|
||||
onChangeTable,
|
||||
onBringToFront,
|
||||
onRemoveTable,
|
||||
onCreateReference,
|
||||
onAddReferenceByColumn,
|
||||
onSelectColumn,
|
||||
onChangeColumn,
|
||||
sourceDragColumn,
|
||||
setSourceDragColumn,
|
||||
targetDragColumn,
|
||||
setTargetDragColumn,
|
||||
onChangeDomTable,
|
||||
wrapperRef,
|
||||
setChangeToken,
|
||||
designer,
|
||||
}) {
|
||||
const { pureName, columns, left, top, designerId, alias, objectTypeField } = table;
|
||||
const [movingPosition, setMovingPosition] = React.useState(null);
|
||||
const movingPositionRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
const domObjectsRef = React.useRef({});
|
||||
const showMenu = useShowMenu();
|
||||
const showModal = useShowModal();
|
||||
|
||||
const moveStartXRef = React.useRef(null);
|
||||
const moveStartYRef = React.useRef(null);
|
||||
|
||||
const handleMove = React.useCallback(e => {
|
||||
let diffX = e.clientX - moveStartXRef.current;
|
||||
let diffY = e.clientY - moveStartYRef.current;
|
||||
moveStartXRef.current = e.clientX;
|
||||
moveStartYRef.current = e.clientY;
|
||||
|
||||
movingPositionRef.current = {
|
||||
left: (movingPositionRef.current.left || 0) + diffX,
|
||||
top: (movingPositionRef.current.top || 0) + diffY,
|
||||
};
|
||||
setMovingPosition(movingPositionRef.current);
|
||||
// setChangeToken((x) => x + 1);
|
||||
changeTokenDebounced.current();
|
||||
// onChangeTable(
|
||||
// {
|
||||
// ...props,
|
||||
// left: (left || 0) + diffX,
|
||||
// top: (top || 0) + diffY,
|
||||
// },
|
||||
// index
|
||||
// );
|
||||
}, []);
|
||||
|
||||
const changeTokenDebounced = React.useRef(
|
||||
// @ts-ignore
|
||||
_.debounce(() => setChangeToken(x => x + 1), 100)
|
||||
);
|
||||
|
||||
const handleMoveEnd = React.useCallback(
|
||||
e => {
|
||||
if (movingPositionRef.current) {
|
||||
onChangeTable({
|
||||
...table,
|
||||
left: movingPositionRef.current.left,
|
||||
top: movingPositionRef.current.top,
|
||||
});
|
||||
}
|
||||
|
||||
movingPositionRef.current = null;
|
||||
setMovingPosition(null);
|
||||
changeTokenDebounced.current();
|
||||
// setChangeToken((x) => x + 1);
|
||||
|
||||
// this.props.model.fixPositions();
|
||||
|
||||
// this.props.designer.changedModel(true);
|
||||
},
|
||||
[onChangeTable, table]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (movingPosition) {
|
||||
document.addEventListener('mousemove', handleMove, true);
|
||||
document.addEventListener('mouseup', handleMoveEnd, true);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMove, true);
|
||||
document.removeEventListener('mouseup', handleMoveEnd, true);
|
||||
};
|
||||
}
|
||||
}, [movingPosition == null, handleMove, handleMoveEnd]);
|
||||
|
||||
const headerMouseDown = React.useCallback(
|
||||
e => {
|
||||
e.preventDefault();
|
||||
moveStartXRef.current = e.clientX;
|
||||
moveStartYRef.current = e.clientY;
|
||||
movingPositionRef.current = { left, top };
|
||||
setMovingPosition(movingPositionRef.current);
|
||||
// setIsMoving(true);
|
||||
},
|
||||
[handleMove, handleMoveEnd]
|
||||
);
|
||||
|
||||
const dispatchDomColumn = (columnName, dom) => {
|
||||
domObjectsRef.current[columnName] = dom;
|
||||
onChangeDomTable(new DomTableRef(table, domObjectsRef.current, wrapperRef.current));
|
||||
changeTokenDebounced.current();
|
||||
};
|
||||
|
||||
const handleSetTableAlias = () => {
|
||||
showModal(modalState => (
|
||||
<InputTextModal
|
||||
modalState={modalState}
|
||||
value={alias || ''}
|
||||
label="New alias"
|
||||
header="Set table alias"
|
||||
onConfirm={newAlias => {
|
||||
onChangeTable({
|
||||
...table,
|
||||
alias: newAlias,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const handleHeaderContextMenu = event => {
|
||||
event.preventDefault();
|
||||
showMenu(
|
||||
event.pageX,
|
||||
event.pageY,
|
||||
<TableContextMenu
|
||||
remove={() => onRemoveTable({ designerId })}
|
||||
setTableAlias={handleSetTableAlias}
|
||||
removeTableAlias={
|
||||
alias
|
||||
? () =>
|
||||
onChangeTable({
|
||||
...table,
|
||||
alias: null,
|
||||
})
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleColumnContextMenu = column => event => {
|
||||
event.preventDefault();
|
||||
const foreignKey = findForeignKeyForColumn(table, column);
|
||||
showMenu(
|
||||
event.pageX,
|
||||
event.pageY,
|
||||
<ColumnContextMenu
|
||||
setSortOrder={sortOrder => {
|
||||
onChangeColumn(
|
||||
{
|
||||
...column,
|
||||
designerId,
|
||||
},
|
||||
col => ({ ...col, sortOrder })
|
||||
);
|
||||
}}
|
||||
addReference={
|
||||
foreignKey
|
||||
? () => {
|
||||
onAddReferenceByColumn(designerId, foreignKey);
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
theme={theme}
|
||||
style={{
|
||||
left: movingPosition ? movingPosition.left : left,
|
||||
top: movingPosition ? movingPosition.top : top,
|
||||
}}
|
||||
onMouseDown={() => onBringToFront(table)}
|
||||
ref={dom => dispatchDomColumn('', dom)}
|
||||
>
|
||||
<Header
|
||||
onMouseDown={headerMouseDown}
|
||||
theme={theme}
|
||||
onContextMenu={handleHeaderContextMenu}
|
||||
// @ts-ignore
|
||||
objectTypeField={objectTypeField}
|
||||
>
|
||||
<HeaderLabel>{alias || pureName}</HeaderLabel>
|
||||
<CloseWrapper onClick={() => onRemoveTable(table)} theme={theme}>
|
||||
<FontIcon icon="icon close" />
|
||||
</CloseWrapper>
|
||||
</Header>
|
||||
<ColumnsWrapper>
|
||||
{(columns || []).map(column => (
|
||||
<ColumnLine
|
||||
onContextMenu={handleColumnContextMenu(column)}
|
||||
key={column.columnName}
|
||||
theme={theme}
|
||||
draggable
|
||||
ref={dom => dispatchDomColumn(column.columnName, dom)}
|
||||
// @ts-ignore
|
||||
isDragSource={
|
||||
sourceDragColumn &&
|
||||
sourceDragColumn.designerId == designerId &&
|
||||
sourceDragColumn.columnName == column.columnName
|
||||
}
|
||||
// @ts-ignore
|
||||
isDragTarget={
|
||||
targetDragColumn &&
|
||||
targetDragColumn.designerId == designerId &&
|
||||
targetDragColumn.columnName == column.columnName
|
||||
}
|
||||
onDragStart={e => {
|
||||
const dragData = {
|
||||
...column,
|
||||
designerId,
|
||||
};
|
||||
setSourceDragColumn(dragData);
|
||||
e.dataTransfer.setData('designer_column_drag_data', JSON.stringify(dragData));
|
||||
}}
|
||||
onDragEnd={e => {
|
||||
setTargetDragColumn(null);
|
||||
setSourceDragColumn(null);
|
||||
}}
|
||||
onDragOver={e => {
|
||||
if (sourceDragColumn) {
|
||||
e.preventDefault();
|
||||
setTargetDragColumn({
|
||||
...column,
|
||||
designerId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDrop={e => {
|
||||
var data = e.dataTransfer.getData('designer_column_drag_data');
|
||||
e.preventDefault();
|
||||
if (!data) return;
|
||||
onCreateReference(sourceDragColumn, targetDragColumn);
|
||||
setTargetDragColumn(null);
|
||||
setSourceDragColumn(null);
|
||||
}}
|
||||
onMouseDown={e =>
|
||||
onSelectColumn({
|
||||
...column,
|
||||
designerId,
|
||||
})
|
||||
}
|
||||
>
|
||||
<CheckboxField
|
||||
checked={
|
||||
!!(designer.columns || []).find(
|
||||
x => x.designerId == designerId && x.columnName == column.columnName && x.isOutput
|
||||
)
|
||||
}
|
||||
onChange={e => {
|
||||
if (e.target.checked) {
|
||||
onChangeColumn(
|
||||
{
|
||||
...column,
|
||||
designerId,
|
||||
},
|
||||
col => ({ ...col, isOutput: true })
|
||||
);
|
||||
} else {
|
||||
onChangeColumn(
|
||||
{
|
||||
...column,
|
||||
designerId,
|
||||
},
|
||||
col => ({ ...col, isOutput: false })
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ColumnLabel {...column} foreignKey={findForeignKeyForColumn(table, column)} forceIcon />
|
||||
<ColumnDesignerIcons column={column} designerId={designerId} designer={designer} />
|
||||
</ColumnLine>
|
||||
))}
|
||||
</ColumnsWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user