mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-20 20:35:59 +00:00
@@ -9,7 +9,7 @@
|
|||||||
"start:api": "yarn workspace dbgate-api start",
|
"start:api": "yarn workspace dbgate-api start",
|
||||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||||
"start:api:covid": "yarn workspace dbgate-api start:covid",
|
"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:sqltree": "yarn workspace dbgate-sqltree start",
|
||||||
"start:tools": "yarn workspace dbgate-tools start",
|
"start:tools": "yarn workspace dbgate-tools start",
|
||||||
"start:datalib": "yarn workspace dbgate-datalib 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;
|
getOutputParams?: (sourceName, values) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ThemeDefinition {
|
||||||
|
className: string;
|
||||||
|
themeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginDefinition {
|
export interface PluginDefinition {
|
||||||
packageName: string;
|
packageName: string;
|
||||||
manifest: any;
|
manifest: any;
|
||||||
@@ -31,4 +36,5 @@ export interface ExtensionsDirectory {
|
|||||||
plugins: PluginDefinition[];
|
plugins: PluginDefinition[];
|
||||||
fileFormats: FileFormatDefinition[];
|
fileFormats: FileFormatDefinition[];
|
||||||
drivers: EngineDriver[];
|
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",
|
"name": "dbgate-web",
|
||||||
"version": "3.9.5",
|
"version": "1.0.0",
|
||||||
"files": [
|
|
||||||
"build"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env BROWSER=none PORT=5000 react-scripts start",
|
"build": "rollup -c",
|
||||||
"build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
"dev": "rollup -c -w",
|
||||||
"build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build",
|
"start": "sirv public",
|
||||||
"build": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
"validate": "svelte-check"
|
||||||
"prepublishOnly": "yarn build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"ts": "tsc"
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^16.9.17",
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
"@types/styled-components": "^4.4.2",
|
"@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",
|
"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-datalib": "^3.9.5",
|
||||||
"dbgate-sqltree": "^3.9.5",
|
"dbgate-sqltree": "^3.9.5",
|
||||||
"dbgate-tools": "^3.9.5",
|
"dbgate-tools": "^3.9.5",
|
||||||
"eslint": "^6.8.0",
|
"lodash": "^4.17.15"
|
||||||
"eslint-plugin-react": "^7.17.0",
|
},
|
||||||
"json-stable-stringify": "^1.0.1",
|
"dependencies": {
|
||||||
"localforage": "^1.9.0",
|
"@mdi/font": "^5.9.55",
|
||||||
"markdown-to-jsx": "^7.1.0",
|
"sirv-cli": "^1.0.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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset='utf-8'>
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta name="description"
|
|
||||||
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
<title>Svelte app</title>
|
||||||
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`.
|
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||||
-->
|
<link rel='stylesheet' href='/global.css'>
|
||||||
<title>DbGate</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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>
|
</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;
|
return this.insertedRows.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
// static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
||||||
return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
// return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
||||||
}
|
// }
|
||||||
static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
// static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
||||||
return [sourceRows, changeSetState ? changeSetState.value : null, 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 visibleRealColumnIndexes = [];
|
||||||
const modelIndexes = {};
|
const modelIndexes = {};
|
||||||
/** @type {(import('dbgate-datalib').DisplayColumn & {widthPx: string; colIndex: number})[]} */
|
/** @type {(import('dbgate-datalib').DisplayColumn & {width: number; colIndex: number})[]} */
|
||||||
const realColumns = [];
|
const realColumns = [];
|
||||||
|
|
||||||
// frozen columns
|
// frozen columns
|
||||||
@@ -112,12 +112,11 @@ export function countVisibleRealColumns(columnSizes, firstVisibleColumnScrollInd
|
|||||||
|
|
||||||
let col = columns[modelColumnIndex];
|
let col = columns[modelColumnIndex];
|
||||||
if (!col) continue;
|
if (!col) continue;
|
||||||
const widthNumber = columnSizes.getSizeByRealIndex(colIndex);
|
const width = columnSizes.getSizeByRealIndex(colIndex);
|
||||||
realColumns.push({
|
realColumns.push({
|
||||||
...col,
|
...col,
|
||||||
colIndex,
|
colIndex,
|
||||||
widthNumber,
|
width,
|
||||||
widthPx: `${widthNumber}px`,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return realColumns;
|
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