mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-28 19:56:00 +00:00
Merge branch 'develop'
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,6 +12,8 @@ node_modules
|
||||
build
|
||||
dist
|
||||
|
||||
app/packages/web/public
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
@@ -24,3 +26,4 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
app/src/nativeModulesContent.js
|
||||
packages/api/src/nativeModulesContent.js
|
||||
.VSCodeCounter
|
||||
@@ -68,10 +68,10 @@
|
||||
"start": "cross-env ELECTRON_START_URL=http://localhost:5000 electron .",
|
||||
"start:local": "cross-env electron .",
|
||||
"dist": "electron-builder",
|
||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn dist",
|
||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn predist",
|
||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
|
||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/build/*\" packages && copyfiles \"../packages/web/build/**/*\" packages"
|
||||
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/public/*\" packages && copyfiles \"../packages/web/public/**/*\" packages"
|
||||
},
|
||||
"main": "src/electron.js",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -27,6 +27,8 @@ autoUpdater.logger = log;
|
||||
// TODO - create settings for this
|
||||
// appUpdater.channel = 'beta';
|
||||
|
||||
let commands = {};
|
||||
|
||||
function hideSplash() {
|
||||
if (splashWindow) {
|
||||
splashWindow.destroy();
|
||||
@@ -35,61 +37,35 @@ function hideSplash() {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
function commandItem(id) {
|
||||
const command = commands[id];
|
||||
return {
|
||||
id,
|
||||
label: command ? command.menuName || command.toolbarName || command.name : id,
|
||||
accelerator: command ? command.keyText : undefined,
|
||||
enabled: command ? command.enabled : false,
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_runCommand('${id}')`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildMenu() {
|
||||
const template = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Connect to database',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Open file',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_openFile()`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('save')`);
|
||||
},
|
||||
accelerator: 'Ctrl+S',
|
||||
id: 'save',
|
||||
},
|
||||
{
|
||||
label: 'Save As',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('saveAs')`);
|
||||
},
|
||||
accelerator: 'Ctrl+Shift+S',
|
||||
id: 'saveAs',
|
||||
},
|
||||
commandItem('new.connection'),
|
||||
commandItem('file.open'),
|
||||
commandItem('group.save'),
|
||||
commandItem('group.saveAs'),
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{
|
||||
label: 'New query',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Close all tabs',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
|
||||
},
|
||||
},
|
||||
{ role: 'minimize' },
|
||||
],
|
||||
submenu: [commandItem('new.query'), { type: 'separator' }, commandItem('tabs.closeAll'), { role: 'minimize' }],
|
||||
},
|
||||
|
||||
// {
|
||||
@@ -144,12 +120,7 @@ function buildMenu() {
|
||||
require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_showAbout()`);
|
||||
},
|
||||
},
|
||||
commandItem('about.show'),
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -157,10 +128,22 @@ function buildMenu() {
|
||||
return Menu.buildFromTemplate(template);
|
||||
}
|
||||
|
||||
ipcMain.on('update-menu', async (event, arg) => {
|
||||
const commands = await mainWindow.webContents.executeJavaScript(`dbgate_getCurrentTabCommands()`);
|
||||
mainMenu.getMenuItemById('save').enabled = !!commands.save;
|
||||
mainMenu.getMenuItemById('saveAs').enabled = !!commands.saveAs;
|
||||
ipcMain.on('update-commands', async (event, arg) => {
|
||||
commands = JSON.parse(arg);
|
||||
for (const key of Object.keys(commands)) {
|
||||
const menu = mainMenu.getMenuItemById(key);
|
||||
if (!menu) continue;
|
||||
const command = commands[key];
|
||||
|
||||
// rebuild menu
|
||||
if (menu.label != command.text || menu.accelerator != command.keyText) {
|
||||
mainMenu = buildMenu();
|
||||
mainWindow.setMenu(mainMenu);
|
||||
return;
|
||||
}
|
||||
|
||||
menu.enabled = command.enabled;
|
||||
}
|
||||
});
|
||||
|
||||
function createWindow() {
|
||||
@@ -186,7 +169,7 @@ function createWindow() {
|
||||
const startUrl =
|
||||
process.env.ELECTRON_START_URL ||
|
||||
url.format({
|
||||
pathname: path.join(__dirname, '../packages/web/build/index.html'),
|
||||
pathname: path.join(__dirname, '../packages/web/public/index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"start:api": "yarn workspace dbgate-api start",
|
||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||
"start:api:covid": "yarn workspace dbgate-api start:covid",
|
||||
"start:web": "yarn workspace dbgate-web start",
|
||||
"start:web": "yarn workspace dbgate-web dev",
|
||||
"start:sqltree": "yarn workspace dbgate-sqltree start",
|
||||
"start:tools": "yarn workspace dbgate-tools start",
|
||||
"start:datalib": "yarn workspace dbgate-datalib start",
|
||||
@@ -21,7 +21,7 @@
|
||||
"build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
||||
"build:app": "cd app && yarn install && yarn build",
|
||||
"build:api": "yarn workspace dbgate-api build",
|
||||
"build:web:docker": "yarn workspace dbgate-web build:docker",
|
||||
"build:web:docker": "yarn workspace dbgate-web build",
|
||||
"build:app:local": "cd app && yarn build:local",
|
||||
"start:app:local": "cd app && yarn start:local",
|
||||
"setCurrentVersion": "node setCurrentVersion",
|
||||
@@ -29,7 +29,7 @@
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --eletron",
|
||||
"prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/build/* docker -u 2 && copyfiles \"packages/web/build/**/*\" docker -u 2",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2",
|
||||
"prepare:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build",
|
||||
"prepare": "yarn build:lib",
|
||||
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",
|
||||
|
||||
@@ -121,7 +121,13 @@ module.exports = {
|
||||
getStats_meta: 'get',
|
||||
getStats({ jslid }) {
|
||||
const file = `${getJslFileName(jslid)}.stats`;
|
||||
if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const hasPermission = require('../utility/hasPermission');
|
||||
|
||||
const preinstallPluginMinimalVersions = {
|
||||
'dbgate-plugin-mssql': '1.1.0',
|
||||
'dbgate-plugin-mysql': '1.1.0',
|
||||
'dbgate-plugin-mysql': '1.1.1',
|
||||
'dbgate-plugin-postgres': '1.1.0',
|
||||
'dbgate-plugin-csv': '1.0.8',
|
||||
'dbgate-plugin-excel': '1.0.6',
|
||||
@@ -149,7 +149,7 @@ module.exports = {
|
||||
return content.commands[command](args);
|
||||
},
|
||||
|
||||
authTypes_meta: 'post',
|
||||
authTypes_meta: 'get',
|
||||
async authTypes({ engine }) {
|
||||
const packageName = extractPackageName(engine);
|
||||
const content = requirePlugin(packageName);
|
||||
|
||||
@@ -347,5 +347,6 @@ export function changeSetInsertNewRow(changeSet: ChangeSet, name?: NamedObjectIn
|
||||
}
|
||||
|
||||
export function changeSetContainsChanges(changeSet: ChangeSet) {
|
||||
if (!changeSet) return false;
|
||||
return changeSet.deletes.length > 0 || changeSet.updates.length > 0 || changeSet.inserts.length > 0;
|
||||
}
|
||||
|
||||
1
packages/types/engines.d.ts
vendored
1
packages/types/engines.d.ts
vendored
@@ -27,6 +27,7 @@ export interface EngineAuthType {
|
||||
export interface EngineDriver {
|
||||
engine: string;
|
||||
title: string;
|
||||
defaultPort?: number;
|
||||
connect({ server, port, user, password, database }): any;
|
||||
query(pool: any, sql: string): Promise<QueryResult>;
|
||||
stream(pool: any, sql: string, options: StreamOptions);
|
||||
|
||||
6
packages/types/extensions.d.ts
vendored
6
packages/types/extensions.d.ts
vendored
@@ -21,6 +21,11 @@ export interface FileFormatDefinition {
|
||||
getOutputParams?: (sourceName, values) => any;
|
||||
}
|
||||
|
||||
export interface ThemeDefinition {
|
||||
className: string;
|
||||
themeName: string;
|
||||
}
|
||||
|
||||
export interface PluginDefinition {
|
||||
packageName: string;
|
||||
manifest: any;
|
||||
@@ -31,4 +36,5 @@ export interface ExtensionsDirectory {
|
||||
plugins: PluginDefinition[];
|
||||
fileFormats: FileFormatDefinition[];
|
||||
drivers: EngineDriver[];
|
||||
themes: ThemeDefinition[];
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/prop-types": "off",
|
||||
"no-unused-vars": "warn"
|
||||
}
|
||||
};
|
||||
105
packages/web/README.md
Normal file
105
packages/web/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
|
||||
|
||||
---
|
||||
|
||||
# svelte app
|
||||
|
||||
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
|
||||
|
||||
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
|
||||
|
||||
```bash
|
||||
npx degit sveltejs/template svelte-app
|
||||
cd svelte-app
|
||||
```
|
||||
|
||||
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
|
||||
|
||||
|
||||
## Get started
|
||||
|
||||
Install the dependencies...
|
||||
|
||||
```bash
|
||||
cd svelte-app
|
||||
npm install
|
||||
```
|
||||
|
||||
...then start [Rollup](https://rollupjs.org):
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
|
||||
|
||||
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
|
||||
|
||||
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
|
||||
|
||||
## Building and running in production mode
|
||||
|
||||
To create an optimised version of the app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
|
||||
|
||||
|
||||
## Single-page app mode
|
||||
|
||||
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
|
||||
|
||||
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
|
||||
|
||||
```js
|
||||
"start": "sirv public --single"
|
||||
```
|
||||
|
||||
## Using TypeScript
|
||||
|
||||
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
|
||||
|
||||
```bash
|
||||
node scripts/setupTypeScript.js
|
||||
```
|
||||
|
||||
Or remove the script via:
|
||||
|
||||
```bash
|
||||
rm scripts/setupTypeScript.js
|
||||
```
|
||||
|
||||
## Deploying to the web
|
||||
|
||||
### With [Vercel](https://vercel.com)
|
||||
|
||||
Install `vercel` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g vercel
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
cd public
|
||||
vercel deploy --name my-project
|
||||
```
|
||||
|
||||
### With [surge](https://surge.sh/)
|
||||
|
||||
Install `surge` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g surge
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
surge public my-project.surge.sh
|
||||
```
|
||||
@@ -1,69 +1,51 @@
|
||||
{
|
||||
"name": "dbgate-web",
|
||||
"version": "3.9.5",
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "cross-env BROWSER=none PORT=5000 react-scripts start",
|
||||
"build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
||||
"build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build",
|
||||
"build": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
||||
"prepublishOnly": "yarn build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"ts": "tsc"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"build": "cross-env API_URL=ORIGIN rollup -c",
|
||||
"dev": "cross-env API_URL=http://localhost:3000 rollup -c -w",
|
||||
"start": "sirv public",
|
||||
"validate": "svelte-check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/styled-components": "^4.4.2",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"typescript": "^3.7.4",
|
||||
"@ant-design/colors": "^5.0.0",
|
||||
"@mdi/font": "^5.8.55",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||
"@rollup/plugin-replace": "^2.4.1",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@tsconfig/svelte": "^1.0.0",
|
||||
"ace-builds": "^1.4.8",
|
||||
"axios": "^0.19.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"dbgate-datalib": "^3.9.5",
|
||||
"dbgate-sqltree": "^3.9.5",
|
||||
"dbgate-tools": "^3.9.5",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"localforage": "^1.9.0",
|
||||
"markdown-to-jsx": "^7.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
"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",
|
||||
"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",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"sql-formatter": "^2.3.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"svelte": "^3.35.0",
|
||||
"svelte-check": "^1.0.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.3",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^5.9.55",
|
||||
"file-selector": "^0.2.4",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sirv-cli": "^1.0.0",
|
||||
"svelte-markdown": "^0.1.4",
|
||||
"svelte-select": "^3.17.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;
|
||||
}
|
||||
22
packages/web/public/dimensions.css
Normal file
22
packages/web/public/dimensions.css
Normal file
@@ -0,0 +1,22 @@
|
||||
:root {
|
||||
--dim-widget-icon-size: 60px;
|
||||
--dim-statusbar-height: 20px;
|
||||
--dim-left-panel-width: 300px;
|
||||
--dim-tabs-panel-height: 53px;
|
||||
--dim-tabs-height: 33px;
|
||||
--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));
|
||||
|
||||
--dim-large-form-margin: 20px;
|
||||
}
|
||||
142
packages/web/public/global.css
Normal file
142
packages/web/public/global.css
Normal file
@@ -0,0 +1,142 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.horizontal-split-handle {
|
||||
background-color: var(--theme-border);
|
||||
width: var(--dim-splitter-thickness);
|
||||
cursor: col-resize;
|
||||
}
|
||||
.horizontal-split-handle:hover {
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
|
||||
.vertical-split-handle {
|
||||
background-color: var(--theme-border);
|
||||
height: var(--dim-splitter-thickness);
|
||||
cursor: row-resize;
|
||||
}
|
||||
.vertical-split-handle:hover {
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
|
||||
.icon-invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
.space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.flex1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.col-9 {
|
||||
flex-basis: 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
.col-8 {
|
||||
flex-basis: 66.6667%;
|
||||
max-width: 66.6667%;
|
||||
}
|
||||
.col-6 {
|
||||
flex-basis: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
.col-4 {
|
||||
flex-basis: 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
}
|
||||
.col-3 {
|
||||
flex-basis: 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.largeFormMarker input[type='text'] {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.largeFormMarker input[type='password'] {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.largeFormMarker select {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar {
|
||||
height: 0.8em;
|
||||
width: 0.8em;
|
||||
}
|
||||
body *::-webkit-scrollbar-track {
|
||||
border-radius: 1px;
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
body *::-webkit-scrollbar-corner {
|
||||
border-radius: 1px;
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar-thumb {
|
||||
border-radius: 1px;
|
||||
background-color: var(--theme-bg-3);
|
||||
}
|
||||
|
||||
body *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--theme-bg-4);
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: var(--theme-bg-0);
|
||||
color: var(--theme-font-1);
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: var(--theme-bg-0);
|
||||
color: var(--theme-font-1);
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
select[disabled] {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: var(--theme-bg-0);
|
||||
color: var(--theme-font-1);
|
||||
border: 1px solid var(--theme-border);
|
||||
}
|
||||
32
packages/web/public/icon-colors.css
Normal file
32
packages/web/public/icon-colors.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.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);
|
||||
}
|
||||
|
||||
|
||||
.color-icon-inv-green {
|
||||
color: var(--theme-icon-inv-green);
|
||||
}
|
||||
|
||||
.color-icon-inv-red {
|
||||
color: var(--theme-icon-inv-red);
|
||||
}
|
||||
@@ -2,43 +2,28 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description"
|
||||
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>DbGate</title>
|
||||
<title>DbGate</title>
|
||||
<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='icon' type='image/png' href='favicon.ico'>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<link rel='stylesheet' href='global.css'>
|
||||
<link rel='stylesheet' href='dimensions.css'>
|
||||
<link rel='stylesheet' href='bulma.css'>
|
||||
<link rel='stylesheet' href='icon-colors.css'>
|
||||
<link rel='stylesheet' href='build/bundle.css'>
|
||||
<link rel='stylesheet' href='build/fonts/materialdesignicons.css'>
|
||||
|
||||
<script defer src='build/bundle.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">Loading DbGate...</div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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>
|
||||
102
packages/web/rollup.config.js
Normal file
102
packages/web/rollup.config.js
Normal file
@@ -0,0 +1,102 @@
|
||||
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 replace from '@rollup/plugin-replace';
|
||||
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/',
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
replace({
|
||||
'process.env.API_URL': JSON.stringify(process.env.API_URL),
|
||||
}),
|
||||
|
||||
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>
|
||||
)
|
||||
);
|
||||
}
|
||||
55
packages/web/src/DragAndDropFileTarget.svelte
Normal file
55
packages/web/src/DragAndDropFileTarget.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import FontIcon from './icons/FontIcon.svelte';
|
||||
|
||||
import { extensions } from './stores';
|
||||
|
||||
import getElectron from './utility/getElectron';
|
||||
|
||||
const electron = getElectron();
|
||||
$: fileTypeNames = _.compact([
|
||||
...$extensions.fileFormats.filter(x => x.readerFunc).map(x => x.name),
|
||||
electron ? 'SQL' : null,
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div class="target">
|
||||
<div>
|
||||
<div class="icon">
|
||||
<FontIcon icon="icon cloud-upload" />
|
||||
</div>
|
||||
<div class="title">Drop the files to upload to DbGate</div>
|
||||
<div class="info">Supported file types: {fileTypeNames.join(', ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.target {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--theme-bg-selected);
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 1000;
|
||||
}
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-size: 50px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.title {
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
137
packages/web/src/Screen.svelte
Normal file
137
packages/web/src/Screen.svelte
Normal file
@@ -0,0 +1,137 @@
|
||||
<script>
|
||||
import WidgetContainer from './widgets/WidgetContainer.svelte';
|
||||
import WidgetIconPanel from './widgets/WidgetIconPanel.svelte';
|
||||
import {
|
||||
currentTheme,
|
||||
isFileDragActive,
|
||||
leftPanelWidth,
|
||||
selectedWidget,
|
||||
visibleCommandPalette,
|
||||
visibleToolbar,
|
||||
} from './stores';
|
||||
import TabsPanel from './widgets/TabsPanel.svelte';
|
||||
import TabRegister from './TabRegister.svelte';
|
||||
import CommandPalette from './commands/CommandPalette.svelte';
|
||||
import Toolbar from './widgets/Toolbar.svelte';
|
||||
import splitterDrag from './utility/splitterDrag';
|
||||
import CurrentDropDownMenu from './modals/CurrentDropDownMenu.svelte';
|
||||
import StatusBar from './widgets/StatusBar.svelte';
|
||||
import ModalLayer from './modals/ModalLayer.svelte';
|
||||
import DragAndDropFileTarget from './DragAndDropFileTarget.svelte';
|
||||
import dragDropFileTarget from './utility/dragDropFileTarget';
|
||||
</script>
|
||||
|
||||
<div class={`${$currentTheme} root`} use:dragDropFileTarget>
|
||||
<div class="iconbar">
|
||||
<WidgetIconPanel />
|
||||
</div>
|
||||
<div class="statusbar">
|
||||
<StatusBar />
|
||||
</div>
|
||||
{#if $selectedWidget}
|
||||
<div class="leftpanel">
|
||||
<WidgetContainer />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="tabs">
|
||||
<TabsPanel />
|
||||
</div>
|
||||
<div class="content">
|
||||
<TabRegister />
|
||||
</div>
|
||||
{#if $selectedWidget}
|
||||
<div
|
||||
class="horizontal-split-handle splitter"
|
||||
use:splitterDrag={'clientX'}
|
||||
on:resizeSplitter={e => leftPanelWidth.update(x => x + e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
{#if $visibleCommandPalette}
|
||||
<div class="commads">
|
||||
<CommandPalette />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $visibleToolbar}
|
||||
<div class="toolbar">
|
||||
<Toolbar />
|
||||
</div>
|
||||
{/if}
|
||||
<CurrentDropDownMenu />
|
||||
<ModalLayer />
|
||||
{#if $isFileDragActive}
|
||||
<DragAndDropFileTarget />
|
||||
{/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;
|
||||
}
|
||||
|
||||
.splitter {
|
||||
position: absolute;
|
||||
top: var(--dim-header-top);
|
||||
bottom: var(--dim-statusbar-height);
|
||||
left: calc(var(--dim-widget-icon-size) + var(--dim-left-panel-width));
|
||||
}
|
||||
</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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
35
packages/web/src/TabContent.svelte
Normal file
35
packages/web/src/TabContent.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let tabid;
|
||||
export let tabVisible;
|
||||
export let tabComponent;
|
||||
|
||||
const tabVisibleStore = writable(tabVisible);
|
||||
setContext('tabid', tabid);
|
||||
setContext('tabVisible', tabVisibleStore);
|
||||
|
||||
$: tabVisibleStore.set(tabVisible);
|
||||
</script>
|
||||
|
||||
<div class:tabVisible>
|
||||
<svelte:component this={tabComponent} {...$$restProps} {tabid} {tabVisible} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
}
|
||||
.tabVisible {
|
||||
visibility: visible;
|
||||
}
|
||||
:not(.tabVisible) {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
62
packages/web/src/TabRegister.svelte
Normal file
62
packages/web/src/TabRegister.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script context="module" lang="ts">
|
||||
function createTabComponent(selectedTab) {
|
||||
const tabComponent = tabs[selectedTab.tabComponent]?.default;
|
||||
if (tabComponent) {
|
||||
return {
|
||||
tabComponent,
|
||||
props: selectedTab && selectedTab.props,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { openedTabs } from './stores';
|
||||
import TabContent from './TabContent.svelte';
|
||||
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]) {
|
||||
const newTab = createTabComponent(selectedTab);
|
||||
if (newTab) {
|
||||
mountedTabs = {
|
||||
...mountedTabs,
|
||||
[tabid]: newTab,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each _.keys(mountedTabs) as tabid (tabid)}
|
||||
<TabContent
|
||||
tabComponent={mountedTabs[tabid].tabComponent}
|
||||
{...mountedTabs[tabid].props}
|
||||
{tabid}
|
||||
tabVisible={tabid == (selectedTab && selectedTab.tabid)}
|
||||
/>
|
||||
{/each}
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
83
packages/web/src/appobj/AppObjectCore.svelte
Normal file
83
packages/web/src/appobj/AppObjectCore.svelte
Normal file
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import contextMenu from '../utility/contextMenu';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let icon;
|
||||
export let title;
|
||||
export let data;
|
||||
|
||||
export let isBold = false;
|
||||
export let isBusy = false;
|
||||
export let statusIcon = undefined;
|
||||
export let statusTitle = undefined;
|
||||
export let extInfo = undefined;
|
||||
export let menu = undefined;
|
||||
export let expandIcon = undefined;
|
||||
|
||||
function handleExpand() {
|
||||
dispatch('expand');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="main"
|
||||
class:isBold
|
||||
draggable={true}
|
||||
on:click
|
||||
use:contextMenu={menu}
|
||||
on:dragstart={e => {
|
||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
||||
}}
|
||||
>
|
||||
{#if expandIcon}
|
||||
<span class="expand-icon" on:click|stopPropagation={handleExpand}>
|
||||
<FontIcon icon={expandIcon} />
|
||||
</span>
|
||||
{/if}
|
||||
{#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);
|
||||
}
|
||||
.expand-icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
</style>
|
||||
48
packages/web/src/appobj/AppObjectGroup.svelte
Normal file
48
packages/web/src/appobj/AppObjectGroup.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script>
|
||||
import { plusExpandIcon } from '../icons/expandIcons';
|
||||
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
import AppObjectListItem from './AppObjectListItem.svelte';
|
||||
|
||||
export let group;
|
||||
export let items;
|
||||
export let module;
|
||||
|
||||
let isExpanded = true;
|
||||
|
||||
$: filtered = items.filter(x => x.isMatched);
|
||||
$: countText = filtered.length < items.length ? `${filtered.length}/${items.length}` : `${items.length}`;
|
||||
</script>
|
||||
|
||||
<div class="group" on:click={() => (isExpanded = !isExpanded)}>
|
||||
<span class="expand-icon">
|
||||
<FontIcon icon={plusExpandIcon(isExpanded)} />
|
||||
</span>
|
||||
|
||||
{group}
|
||||
{items && `(${countText})`}
|
||||
</div>
|
||||
|
||||
{#if isExpanded}
|
||||
{#each filtered as item (module.extractKey(item.data))}
|
||||
<AppObjectListItem {...$$restProps} {module} data={item.data} on:objectClick />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.group {
|
||||
user-select: none;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.group:hover {
|
||||
background-color: var(--theme-bg-hover);
|
||||
}
|
||||
.expand-icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
</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);
|
||||
});
|
||||
}
|
||||
53
packages/web/src/appobj/AppObjectList.svelte
Normal file
53
packages/web/src/appobj/AppObjectList.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import AppObjectGroup from './AppObjectGroup.svelte';
|
||||
|
||||
import AppObjectListItem from './AppObjectListItem.svelte';
|
||||
|
||||
export let list;
|
||||
export let module;
|
||||
export let subItemsComponent = undefined;
|
||||
export let expandOnClick = false;
|
||||
export let isExpandable = undefined;
|
||||
export let filter;
|
||||
export let expandIconFunc = undefined;
|
||||
|
||||
export let groupFunc = undefined;
|
||||
|
||||
$: filtered = list.filter(data => {
|
||||
const matcher = module.createMatcher && module.createMatcher(data);
|
||||
if (matcher && !matcher(filter)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
$: listGrouped = groupFunc
|
||||
? _.compact(
|
||||
(list || []).map(data => {
|
||||
const matcher = module.createMatcher && module.createMatcher(data);
|
||||
const isMatched = matcher && !matcher(filter) ? false : true;
|
||||
const group = groupFunc(data);
|
||||
return { group, data, isMatched };
|
||||
})
|
||||
)
|
||||
: null;
|
||||
|
||||
$: groups = groupFunc ? _.groupBy(listGrouped, 'group') : null;
|
||||
</script>
|
||||
|
||||
{#if groupFunc}
|
||||
{#each _.keys(groups) as group (group)}
|
||||
<AppObjectGroup {group} {module} items={groups[group]} {expandIconFunc} {isExpandable} {subItemsComponent} />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each filtered as data (module.extractKey(data))}
|
||||
<AppObjectListItem
|
||||
{module}
|
||||
{subItemsComponent}
|
||||
{expandOnClick}
|
||||
{data}
|
||||
{isExpandable}
|
||||
on:objectClick
|
||||
{expandIconFunc}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
56
packages/web/src/appobj/AppObjectListItem.svelte
Normal file
56
packages/web/src/appobj/AppObjectListItem.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" context="module">
|
||||
function getExpandIcon(expandable, subItemsComponent, isExpanded, expandIconFunc) {
|
||||
if (!subItemsComponent) return null;
|
||||
if (!expandable) return 'icon invisible-box';
|
||||
return expandIconFunc(isExpanded);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import { plusExpandIcon } from '../icons/expandIcons';
|
||||
|
||||
export let module;
|
||||
export let data;
|
||||
export let subItemsComponent;
|
||||
export let expandOnClick;
|
||||
export let isExpandable = undefined;
|
||||
export let expandIconFunc = plusExpandIcon;
|
||||
|
||||
let isExpanded = false;
|
||||
|
||||
async function handleExpand() {
|
||||
if (subItemsComponent && expandOnClick) {
|
||||
await tick();
|
||||
isExpanded = !isExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
function handleExpandButton() {
|
||||
isExpanded = !isExpanded;
|
||||
}
|
||||
|
||||
$: expandable = data && isExpandable && isExpandable(data);
|
||||
|
||||
$: if (!expandable && isExpanded) isExpanded = false;
|
||||
</script>
|
||||
|
||||
<svelte:component
|
||||
this={module.default}
|
||||
{data}
|
||||
on:click={handleExpand}
|
||||
on:expand={handleExpandButton}
|
||||
expandIcon={getExpandIcon(expandable, subItemsComponent, isExpanded, expandIconFunc)}
|
||||
/>
|
||||
|
||||
{#if isExpanded && subItemsComponent}
|
||||
<div class="subitems">
|
||||
<svelte:component this={subItemsComponent} {data} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.subitems {
|
||||
margin-left: 28px;
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
74
packages/web/src/appobj/ArchiveFileAppObject.svelte
Normal file
74
packages/web/src/appobj/ArchiveFileAppObject.svelte
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" context="module">
|
||||
function openArchive(fileName, folderName) {
|
||||
openNewTab({
|
||||
title: fileName,
|
||||
icon: 'img archive',
|
||||
tooltip: `${folderName}\n${fileName}`,
|
||||
tabComponent: 'ArchiveFileTab',
|
||||
props: {
|
||||
archiveFile: fileName,
|
||||
archiveFolder: folderName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const extractKey = data => data.fileName;
|
||||
export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
|
||||
import { currentArchive } from '../stores';
|
||||
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
const handleDelete = () => {
|
||||
axiosInstance.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
|
||||
};
|
||||
const handleOpenRead = () => {
|
||||
openArchive(data.fileName, data.folderName);
|
||||
};
|
||||
const handleOpenWrite = () => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleClick = () => {
|
||||
openArchive(data.fileName, data.folderName);
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ text: 'Open (readonly)', onClick: handleOpenRead },
|
||||
{ text: 'Open in free table editor', onClick: handleOpenWrite },
|
||||
{ text: 'Delete', onClick: handleDelete },
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
title={data.fileName}
|
||||
icon="img archive"
|
||||
menu={createMenu}
|
||||
on:click={handleClick}
|
||||
/>
|
||||
@@ -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;
|
||||
33
packages/web/src/appobj/ArchiveFolderAppObject.svelte
Normal file
33
packages/web/src/appobj/ArchiveFolderAppObject.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = data => data.name;
|
||||
export const createMatcher = data => filter => filterName(filter, data.name);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
|
||||
import { currentArchive } from '../stores';
|
||||
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
const handleDelete = () => {
|
||||
axiosInstance.post('archive/delete-folder', { folder: data.name });
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
return [data.name != 'default' && { text: 'Delete', onClick: handleDelete }];
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
title={data.name}
|
||||
icon="img archive-folder"
|
||||
isBold={data.name == $currentArchive}
|
||||
on:click={() => ($currentArchive = data.name)}
|
||||
menu={createMenu}
|
||||
/>
|
||||
@@ -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;
|
||||
70
packages/web/src/appobj/ClosedTabAppObject.svelte
Normal file
70
packages/web/src/appobj/ClosedTabAppObject.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = data => data.tabid;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { openedTabs } from '../stores';
|
||||
import { setSelectedTabFunc } from '../utility/common';
|
||||
import moment from 'moment';
|
||||
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
const handleDelete = () => {
|
||||
openedTabs.update(tabs => tabs.filter(x => x.tabid != data.tabid));
|
||||
};
|
||||
const handleDeleteOlder = () => {
|
||||
openedTabs.update(tabs => tabs.filter(x => !x.closedTime || x.closedTime >= data.closedTime));
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
openedTabs.update(files =>
|
||||
setSelectedTabFunc(
|
||||
files.map(x => ({
|
||||
...x,
|
||||
closedTime: x.tabid == data.tabid ? undefined : x.closedTime,
|
||||
})),
|
||||
data.tabid
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ text: 'Delete', onClick: handleDelete },
|
||||
{ text: 'Delete older', onClick: handleDeleteOlder },
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
title={`${data.title} ${moment(data.closedTime).fromNow()}`}
|
||||
icon={data.icon}
|
||||
isBold={!!data.selected}
|
||||
on:click={onClick}
|
||||
isBusy={data.busy}
|
||||
menu={createMenu}
|
||||
>
|
||||
{#if data.props && data.props.database}
|
||||
<div class="info">
|
||||
<FontIcon icon="icon database" />
|
||||
{data.props.database}
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.contentPreview}
|
||||
<div class="info">
|
||||
{data.contentPreview}
|
||||
</div>
|
||||
{/if}
|
||||
</AppObjectCore>
|
||||
|
||||
<style>
|
||||
.info {
|
||||
margin-left: 30px;
|
||||
color: var(--theme-font-3);
|
||||
}
|
||||
</style>
|
||||
22
packages/web/src/appobj/ColumnAppObject.svelte
Normal file
22
packages/web/src/appobj/ColumnAppObject.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = ({ columnName }) => columnName;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getColumnIcon } from '../elements/ColumnLabel.svelte';
|
||||
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
$: extInfo = data.foreignKey ? `${data.dataType} -> ${data.foreignKey.refTableName}` : data.dataType;
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
title={data.columnName}
|
||||
{extInfo}
|
||||
icon={getColumnIcon(data, true)}
|
||||
disableHover
|
||||
/>
|
||||
@@ -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;
|
||||
116
packages/web/src/appobj/ConnectionAppObject.svelte
Normal file
116
packages/web/src/appobj/ConnectionAppObject.svelte
Normal file
@@ -0,0 +1,116 @@
|
||||
<script context="module">
|
||||
const getContextMenu = (data, $openedConnections) => () => {
|
||||
const handleRefresh = () => {
|
||||
axiosInstance.post('server-connections/refresh', { conid: data._id });
|
||||
};
|
||||
const handleDisconnect = () => {
|
||||
openedConnections.update(list => list.filter(x => x != data._id));
|
||||
};
|
||||
const handleConnect = () => {
|
||||
openedConnections.update(list => _.uniq([...list, data._id]));
|
||||
};
|
||||
const handleEdit = () => {
|
||||
showModal(ConnectionModal, { connection: data });
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
text: 'Edit',
|
||||
onClick: handleEdit,
|
||||
},
|
||||
!$openedConnections.includes(data._id) && {
|
||||
text: 'Connect',
|
||||
onClick: handleConnect,
|
||||
},
|
||||
$openedConnections.includes(data._id) &&
|
||||
data.status && {
|
||||
text: 'Refresh',
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
$openedConnections.includes(data._id) && {
|
||||
text: 'Disconnect',
|
||||
onClick: handleDisconnect,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const extractKey = data => data._id;
|
||||
export const createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
import { currentDatabase, extensions, openedConnections } from '../stores';
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ConnectionModal from '../modals/ConnectionModal.svelte';
|
||||
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
statusIcon = null;
|
||||
statusTitle = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 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} />);
|
||||
// };
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
title={data.displayName || data.server}
|
||||
icon="img server"
|
||||
isBold={_.get($currentDatabase, 'connection._id') == data._id}
|
||||
statusIcon={statusIcon || engineStatusIcon}
|
||||
statusTitle={statusTitle || engineStatusTitle}
|
||||
{extInfo}
|
||||
menu={getContextMenu(data, $openedConnections)}
|
||||
on:click={() => ($openedConnections = _.uniq([...$openedConnections, data._id]))}
|
||||
on:click
|
||||
/>
|
||||
@@ -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;
|
||||
23
packages/web/src/appobj/DatabaseAppObject.svelte
Normal file
23
packages/web/src/appobj/DatabaseAppObject.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = props => props.name;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
|
||||
import { currentDatabase } from '../stores';
|
||||
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{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;
|
||||
310
packages/web/src/appobj/DatabaseObjectAppObject.svelte
Normal file
310
packages/web/src/appobj/DatabaseObjectAppObject.svelte
Normal file
@@ -0,0 +1,310 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
||||
export const createMatcher = ({ pureName }) => filter => filterName(filter, pureName);
|
||||
|
||||
const icons = {
|
||||
tables: 'img table',
|
||||
views: 'img view',
|
||||
procedures: 'img procedure',
|
||||
functions: 'img function',
|
||||
};
|
||||
|
||||
const defaultTabs = {
|
||||
tables: 'TableDataTab',
|
||||
views: 'ViewDataTab',
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
isExport: true,
|
||||
},
|
||||
{
|
||||
label: 'Open in free table editor',
|
||||
isOpenFreeTable: true,
|
||||
},
|
||||
{
|
||||
label: 'Open active chart',
|
||||
isActiveChart: true,
|
||||
},
|
||||
{
|
||||
divider: 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,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
isExport: true,
|
||||
},
|
||||
{
|
||||
label: 'Open in free table editor',
|
||||
isOpenFreeTable: true,
|
||||
},
|
||||
{
|
||||
label: 'Open active chart',
|
||||
isActiveChart: true,
|
||||
},
|
||||
{
|
||||
divider: 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export async function openDatabaseObjectDetail(
|
||||
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 }
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
import { currentDatabase, extensions, openedConnections } from '../stores';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import fullDisplayName from '../utility/fullDisplayName';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
export let data;
|
||||
|
||||
function handleClick() {
|
||||
const { schemaName, pureName, conid, database, objectTypeField } = data;
|
||||
|
||||
openDatabaseObjectDetail(
|
||||
defaultTabs[objectTypeField],
|
||||
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
|
||||
{
|
||||
schemaName,
|
||||
pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField,
|
||||
},
|
||||
false,
|
||||
null
|
||||
);
|
||||
|
||||
// openNewTab({
|
||||
// title: data.pureName,
|
||||
// icon: 'img table',
|
||||
// tabComponent: 'TableDataTab',
|
||||
// props: {
|
||||
// schemaName,
|
||||
// pureName,
|
||||
// conid,
|
||||
// database,
|
||||
// objectTypeField,
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
const getDriver = async () => {
|
||||
const conn = await getConnectionInfo(data);
|
||||
if (!conn) return;
|
||||
const driver = findEngineDriver(conn, $extensions);
|
||||
return driver;
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
const { objectTypeField } = data;
|
||||
return menus[objectTypeField].map(menu => {
|
||||
if (menu.divider) return menu;
|
||||
return {
|
||||
text: menu.label,
|
||||
onClick: async () => {
|
||||
if (menu.isExport) {
|
||||
showModal(ImportExportModal, {
|
||||
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(menu.tab, menu.sqlTemplate, data, menu.forceNewTab, menu.initialData);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
title={data.schemaName ? `${data.schemaName}.${data.pureName}` : data.pureName}
|
||||
icon={icons[data.objectTypeField]}
|
||||
menu={createMenu}
|
||||
on:click={handleClick}
|
||||
on:expand
|
||||
/>
|
||||
@@ -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;
|
||||
100
packages/web/src/appobj/FavoriteFileAppObject.svelte
Normal file
100
packages/web/src/appobj/FavoriteFileAppObject.svelte
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts" context="module">
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
|
||||
export async function openFavorite(favorite) {
|
||||
const { icon, tabComponent, title, props, tabdata } = favorite;
|
||||
let tabdataNew = tabdata;
|
||||
if (props.savedFile) {
|
||||
const resp = await axiosInstance.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
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import getElectron from '../utility/getElectron';
|
||||
|
||||
export let data;
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
const editFavorite = () => {
|
||||
// showModal(modalState => <FavoriteModal modalState={modalState} editingData={data} />);
|
||||
};
|
||||
|
||||
const editFavoriteJson = async () => {
|
||||
const resp = await axiosInstance.post('files/load', {
|
||||
folder: 'favorites',
|
||||
file: data.file,
|
||||
format: 'text',
|
||||
});
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
icon: 'icon favorite',
|
||||
title: data.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=${data.urlPath}`);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete favorite ${data.title}?`,
|
||||
onConfirm: () => {
|
||||
axiosInstance.post('files/delete', { file: data.file, folder: 'favorites' });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ text: 'Delete', onClick: handleDelete },
|
||||
{ text: 'Edit', onClick: editFavorite },
|
||||
{ text: 'Edit JSON definition', onClick: editFavoriteJson },
|
||||
!electron && data.urlPath && { text: 'Copy link', onClick: copyLink },
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
icon={data.icon || 'img favorite'}
|
||||
title={data.title}
|
||||
menu={createMenu}
|
||||
on:click={() => openFavorite(data)}
|
||||
/>
|
||||
@@ -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;
|
||||
23
packages/web/src/appobj/MacroAppObject.svelte
Normal file
23
packages/web/src/appobj/MacroAppObject.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = data => data.name;
|
||||
export const createMatcher = ({ name, title }) => filter => filterName(filter, name, title);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
|
||||
export let data;
|
||||
const selectedMacro = getContext('selectedMacro') as any;
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
title={data.title}
|
||||
icon="img macro"
|
||||
isBold={$selectedMacro?.name == data.name}
|
||||
on:click={() => ($selectedMacro = data)}
|
||||
/>
|
||||
@@ -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;
|
||||
});
|
||||
154
packages/web/src/appobj/SavedFileAppObject.svelte
Normal file
154
packages/web/src/appobj/SavedFileAppObject.svelte
Normal file
@@ -0,0 +1,154 @@
|
||||
<script lang="ts" context="module">
|
||||
interface FileTypeHandler {
|
||||
icon: string;
|
||||
format: string;
|
||||
tabComponent: string;
|
||||
folder: string;
|
||||
currentConnection: boolean;
|
||||
}
|
||||
|
||||
const sql: FileTypeHandler = {
|
||||
icon: 'img sql-file',
|
||||
format: 'text',
|
||||
tabComponent: 'QueryTab',
|
||||
folder: 'sql',
|
||||
currentConnection: true,
|
||||
};
|
||||
|
||||
const shell: FileTypeHandler = {
|
||||
icon: 'img shell',
|
||||
format: 'text',
|
||||
tabComponent: 'ShellTab',
|
||||
folder: 'shell',
|
||||
currentConnection: false,
|
||||
};
|
||||
|
||||
const markdown: FileTypeHandler = {
|
||||
icon: 'img markdown',
|
||||
format: 'text',
|
||||
tabComponent: 'MarkdownEditorTab',
|
||||
folder: 'markdown',
|
||||
currentConnection: false,
|
||||
};
|
||||
|
||||
const charts: FileTypeHandler = {
|
||||
icon: 'img chart',
|
||||
format: 'json',
|
||||
tabComponent: 'ChartTab',
|
||||
folder: 'charts',
|
||||
currentConnection: true,
|
||||
};
|
||||
|
||||
const query: FileTypeHandler = {
|
||||
icon: 'img query-design',
|
||||
format: 'json',
|
||||
tabComponent: 'QueryDesignTab',
|
||||
folder: 'query',
|
||||
currentConnection: true,
|
||||
};
|
||||
|
||||
const HANDLERS = {
|
||||
sql,
|
||||
shell,
|
||||
markdown,
|
||||
charts,
|
||||
query,
|
||||
};
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
|
||||
import { currentDatabase } from '../stores';
|
||||
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
$: folder = data?.folder;
|
||||
$: handler = HANDLERS[folder] as FileTypeHandler;
|
||||
|
||||
const showMarkdownPage = () => {
|
||||
openNewTab({
|
||||
title: data.file,
|
||||
icon: 'img markdown',
|
||||
tabComponent: 'MarkdownViewTab',
|
||||
props: {
|
||||
savedFile: data.file,
|
||||
savedFolder: 'markdown',
|
||||
savedFormat: 'text',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ text: 'Open', onClick: openTab },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Rename', onClick: handleRename },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Delete', onClick: handleDelete },
|
||||
folder == 'markdown' && { text: 'Show page', onClick: showMarkdownPage },
|
||||
];
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete file ${data.file}?`,
|
||||
onConfirm: () => {
|
||||
axiosInstance.post('files/delete', data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRename = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: data.file,
|
||||
label: 'New file name',
|
||||
header: 'Rename file',
|
||||
onConfirm: newFile => {
|
||||
axiosInstance.post('files/rename', { ...data, newFile });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function openTab() {
|
||||
const resp = await axiosInstance.post('files/load', { folder, file: data.file, format: handler.format });
|
||||
|
||||
const connProps: any = {};
|
||||
let tooltip = undefined;
|
||||
|
||||
if (handler.currentConnection) {
|
||||
const connection = _.get($currentDatabase, 'connection') || {};
|
||||
const database = _.get($currentDatabase, 'name');
|
||||
connProps.conid = connection._id;
|
||||
connProps.database = database;
|
||||
tooltip = `${connection.displayName || connection.server}\n${database}`;
|
||||
}
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: data.file,
|
||||
icon: handler.icon,
|
||||
tabComponent: handler.tabComponent,
|
||||
tooltip,
|
||||
props: {
|
||||
savedFile: data.file,
|
||||
savedFolder: handler.folder,
|
||||
savedFormat: handler.format,
|
||||
...connProps,
|
||||
},
|
||||
},
|
||||
{ editor: resp.data }
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppObjectCore {...$$restProps} {data} icon={handler?.icon} title={data?.file} menu={createMenu()} on:click={openTab} />
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
packages/web/src/appobj/SubColumnParamList.svelte
Normal file
16
packages/web/src/appobj/SubColumnParamList.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { findForeignKeyForColumn } from 'dbgate-tools';
|
||||
|
||||
import AppObjectList from './AppObjectList.svelte';
|
||||
import * as columnAppObject from './ColumnAppObject.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<AppObjectList
|
||||
list={(data.columns || []).map(col => ({
|
||||
...col,
|
||||
foreignKey: findForeignKeyForColumn(data, col),
|
||||
}))}
|
||||
module={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 * as databaseAppObject from './DatabaseAppObject.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
$: databases = useDatabaseList({ conid: data._id });
|
||||
</script>
|
||||
|
||||
<AppObjectList list={($databases || []).map(db => ({ ...db, connection: data }))} module={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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
36
packages/web/src/charts/ChartCore.svelte
Normal file
36
packages/web/src/charts/ChartCore.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { onMount, afterUpdate, onDestroy } from 'svelte';
|
||||
import Chart from 'chart.js';
|
||||
import contextMenu from '../utility/contextMenu';
|
||||
|
||||
export let data;
|
||||
export let type = 'line';
|
||||
export let options = {};
|
||||
export let plugins = {};
|
||||
export let menu;
|
||||
|
||||
let chart = null;
|
||||
let domChart;
|
||||
|
||||
onMount(() => {
|
||||
chart = new Chart(domChart, {
|
||||
type,
|
||||
data,
|
||||
options,
|
||||
plugins,
|
||||
});
|
||||
});
|
||||
afterUpdate(() => {
|
||||
if (!chart) return;
|
||||
chart.data = data;
|
||||
chart.type = type;
|
||||
chart.options = options;
|
||||
chart.plugins = plugins;
|
||||
chart.update();
|
||||
});
|
||||
onDestroy(() => {
|
||||
chart = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<canvas bind:this={domChart} {...$$restProps} use:contextMenu={menu} />
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
160
packages/web/src/charts/ChartEditor.svelte
Normal file
160
packages/web/src/charts/ChartEditor.svelte
Normal file
@@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import AboutModal from '../modals/AboutModal.svelte';
|
||||
import { presetPrimaryColors } from '@ant-design/colors';
|
||||
import { startCase } from 'lodash';
|
||||
import FormProviderCore from '../forms/FormProviderCore.svelte';
|
||||
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
|
||||
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
|
||||
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
|
||||
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
|
||||
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import FormFieldTemplateTiny from '../forms/FormFieldTemplateTiny.svelte';
|
||||
import createRef from '../utility/createRef';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import { extensions } from '../stores';
|
||||
import { loadChartData, loadChartStructure } from './chartDataLoader';
|
||||
import DataChart from './DataChart.svelte';
|
||||
import _ from 'lodash';
|
||||
|
||||
export let data;
|
||||
export let configStore;
|
||||
export let conid;
|
||||
export let database;
|
||||
export let sql;
|
||||
export let menu;
|
||||
|
||||
let availableColumnNames = [];
|
||||
let error = null;
|
||||
let loadedData = null;
|
||||
|
||||
$: config = $configStore;
|
||||
|
||||
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);
|
||||
availableColumnNames = columns;
|
||||
} catch (err) {
|
||||
error = 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;
|
||||
loadedData = {
|
||||
structure: columns,
|
||||
rows,
|
||||
};
|
||||
};
|
||||
|
||||
$: {
|
||||
$extensions;
|
||||
if (sql && conid && database) {
|
||||
handleLoadColumns();
|
||||
}
|
||||
}
|
||||
$: {
|
||||
if (data) {
|
||||
availableColumnNames = data.structure.columns.map(x => x.columnName);
|
||||
}
|
||||
}
|
||||
$: {
|
||||
$extensions;
|
||||
if (config.labelColumn && sql && conid && database) {
|
||||
handleLoadData();
|
||||
}
|
||||
}
|
||||
|
||||
let managerSize;
|
||||
</script>
|
||||
|
||||
<FormProviderCore values={configStore} template={FormFieldTemplateTiny}>
|
||||
<HorizontalSplitter initialValue="300px" bind:size={managerSize}>
|
||||
<div class="left" slot="1">
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Style" name="style" height="40%">
|
||||
<ManagerInnerContainer width={managerSize}>
|
||||
<FormSelectField
|
||||
label="Chart type"
|
||||
name="chartType"
|
||||
isNative
|
||||
options={[
|
||||
{ value: 'bar', label: 'Bar' },
|
||||
{ value: 'line', label: 'Line' },
|
||||
{ value: 'pie', label: 'Pie' },
|
||||
{ value: 'polarArea', label: 'Polar area' },
|
||||
]}
|
||||
/>
|
||||
<FormTextField label="Color set" name="colorSeed" />
|
||||
<FormSelectField
|
||||
label="Truncate from"
|
||||
name="truncateFrom"
|
||||
isNative
|
||||
options={[
|
||||
{ value: 'begin', label: 'Begin' },
|
||||
{ value: 'end', label: 'End (most recent data for datetime)' },
|
||||
]}
|
||||
/>
|
||||
<FormTextField label="Truncate limit" name="truncateLimit" />
|
||||
<FormCheckboxField label="Show relative values" name="showRelativeValues" />
|
||||
</ManagerInnerContainer>
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Data" name="data">
|
||||
<ManagerInnerContainer width={managerSize}>
|
||||
{#if availableColumnNames.length > 0}
|
||||
<FormSelectField
|
||||
label="Label column"
|
||||
name="labelColumn"
|
||||
isNative
|
||||
options={[{ value: '' }, ...availableColumnNames.map(col => ({ value: col, label: col }))]}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each availableColumnNames as col (col)}
|
||||
<FormCheckboxField label={col} name={`dataColumn_${col}`} />
|
||||
{#if config[`dataColumn_${col}`]}
|
||||
<FormSelectField
|
||||
label="Color"
|
||||
name={`dataColumnColor_${col}`}
|
||||
isNative
|
||||
options={[
|
||||
{ value: '', label: 'Random' },
|
||||
..._.keys(presetPrimaryColors).map(color => ({ value: color, label: _.startCase(color) })),
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</ManagerInnerContainer>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="2">
|
||||
<DataChart data={data || loadedData} {menu} />
|
||||
</svelte:fragment>
|
||||
</HorizontalSplitter>
|
||||
</FormProviderCore>
|
||||
|
||||
<style>
|
||||
.left {
|
||||
background-color: var(--theme-bg-0);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
143
packages/web/src/charts/DataChart.svelte
Normal file
143
packages/web/src/charts/DataChart.svelte
Normal file
@@ -0,0 +1,143 @@
|
||||
<script lang="ts" context="module">
|
||||
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) {
|
||||
if (!freeData || !labelColumn || !dataColumns || !freeData.rows || dataColumns.length == 0) return null;
|
||||
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 = presetPalettes[color][4] + '80';
|
||||
borderColor = presetPalettes[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];
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import randomcolor from 'randomcolor';
|
||||
import moment from 'moment';
|
||||
import ChartCore from './ChartCore.svelte';
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
import { generate, presetPalettes, presetDarkPalettes, presetPrimaryColors } from '@ant-design/colors';
|
||||
import { extractDataColumnColors, extractDataColumns } from './chartDataLoader';
|
||||
|
||||
export let data;
|
||||
export let menu;
|
||||
|
||||
const { values } = getFormContext();
|
||||
|
||||
let clientWidth;
|
||||
let clientHeight;
|
||||
|
||||
$: dataColumns = extractDataColumns($values);
|
||||
$: dataColumnColors = extractDataColumnColors($values, dataColumns);
|
||||
|
||||
$: chartData = createChartData(
|
||||
data,
|
||||
$values.labelColumn,
|
||||
dataColumns,
|
||||
$values.colorSeed || '5',
|
||||
$values.chartType,
|
||||
dataColumnColors
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="wrapper" bind:clientWidth bind:clientHeight>
|
||||
{#if chartData}
|
||||
{#key `${$values.chartType}|${clientWidth}|${clientHeight}`}
|
||||
<ChartCore
|
||||
width={clientWidth}
|
||||
height={clientHeight}
|
||||
data={chartData[0]}
|
||||
type={$values.chartType}
|
||||
options={chartData[1]}
|
||||
{menu}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,7 @@
|
||||
import { dumpSqlSelect, Select } from 'dbgate-sqltree';
|
||||
import { EngineDriver } from 'dbgate-types';
|
||||
import axios from '../utility/axios';
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import _ from 'lodash';
|
||||
import { extractDataColumns } from './DataChart';
|
||||
|
||||
export async function loadChartStructure(driver: EngineDriver, conid, database, sql) {
|
||||
const select: Select = {
|
||||
@@ -17,7 +16,7 @@ export async function loadChartStructure(driver: EngineDriver, conid, database,
|
||||
|
||||
const dmp = driver.createDumper();
|
||||
dumpSqlSelect(dmp, select);
|
||||
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
||||
const resp = await axiosInstance.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);
|
||||
}
|
||||
@@ -75,7 +74,7 @@ export async function loadChartData(driver: EngineDriver, conid, database, sql,
|
||||
|
||||
const dmp = driver.createDumper();
|
||||
dumpSqlSelect(dmp, select);
|
||||
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
||||
const resp = await axiosInstance.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
||||
let { rows, columns } = resp.data;
|
||||
if (truncateFrom == 'end' && rows) {
|
||||
rows = _.reverse([...rows]);
|
||||
@@ -103,3 +102,21 @@ export async function loadChartData(driver: EngineDriver, conid, database, sql,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
53
packages/web/src/commands/CommandListener.svelte
Normal file
53
packages/web/src/commands/CommandListener.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" context="module">
|
||||
import { commands } from '../stores';
|
||||
import { get } from 'svelte/store';
|
||||
import { runGroupCommand } from './runCommand';
|
||||
|
||||
export function handleCommandKeyDown(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 commandsFiltered: any = Object.values(commandsValue).filter(
|
||||
(x: any) =>
|
||||
x.keyText &&
|
||||
x.keyText
|
||||
.toLowerCase()
|
||||
.split('|')
|
||||
.map(x => x.trim())
|
||||
.includes(keyText.toLowerCase()) &&
|
||||
(x.disableHandleKeyText == null ||
|
||||
!x.disableHandleKeyText
|
||||
.toLowerCase()
|
||||
.split('|')
|
||||
.map(x => x.trim())
|
||||
.includes(keyText.toLowerCase()))
|
||||
);
|
||||
|
||||
if (commandsFiltered.length > 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const notGroup = commandsFiltered.filter(x => x.enabled && !x.isGroupCommand);
|
||||
if (notGroup.length == 1) {
|
||||
const command = notGroup[0];
|
||||
if (command.onClick) command.onClick();
|
||||
return;
|
||||
}
|
||||
|
||||
const group = commandsFiltered.filter(x => x.enabled && x.isGroupCommand);
|
||||
|
||||
if (group.length == 1) {
|
||||
const command = group[0];
|
||||
runGroupCommand(command.group);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleCommandKeyDown} />
|
||||
112
packages/web/src/commands/CommandPalette.svelte
Normal file
112
packages/web/src/commands/CommandPalette.svelte
Normal file
@@ -0,0 +1,112 @@
|
||||
<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),
|
||||
testEnabled: () => !getVisibleCommandPalette(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { derived } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { commands, getVisibleCommandPalette, 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(() => {
|
||||
const oldFocus = document.activeElement;
|
||||
domInput.focus();
|
||||
return () => {
|
||||
if (oldFocus) oldFocus.focus();
|
||||
};
|
||||
});
|
||||
|
||||
$: sortedComands = _.sortBy(
|
||||
Object.values($commands).filter(x => x.enabled),
|
||||
'text'
|
||||
);
|
||||
|
||||
$: filteredItems = (parentCommand ? parentCommand.getSubCommands() : sortedComands).filter(
|
||||
x => !x.isGroupCommand && filterName(filter, x.text)
|
||||
);
|
||||
|
||||
function handleCommand(command) {
|
||||
if (command.getSubCommands) {
|
||||
parentCommand = command;
|
||||
domInput.focus();
|
||||
filter = '';
|
||||
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>
|
||||
67
packages/web/src/commands/invalidateCommands.ts
Normal file
67
packages/web/src/commands/invalidateCommands.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { tick } from 'svelte';
|
||||
import { commands } from '../stores';
|
||||
import { GlobalCommand } from './registerCommand';
|
||||
|
||||
let isInvalidated = false;
|
||||
|
||||
export default async function invalidateCommands() {
|
||||
if (isInvalidated) return;
|
||||
isInvalidated = true;
|
||||
await tick();
|
||||
|
||||
isInvalidated = false;
|
||||
|
||||
commands.update(dct => {
|
||||
let res = null;
|
||||
for (const command of Object.values(dct) as GlobalCommand[]) {
|
||||
if (command.isGroupCommand) continue;
|
||||
const { testEnabled } = command;
|
||||
let enabled = command.enabled;
|
||||
if (testEnabled) enabled = testEnabled();
|
||||
if (enabled != command.enabled) {
|
||||
if (!res) res = { ...dct };
|
||||
res[command.id].enabled = enabled;
|
||||
}
|
||||
}
|
||||
if (res) {
|
||||
const values = Object.values(res) as GlobalCommand[];
|
||||
// test enabled for group commands
|
||||
for (const command of values) {
|
||||
if (!command.isGroupCommand) continue;
|
||||
const groupSources = values.filter(x => x.group == command.group && !x.isGroupCommand && x.enabled);
|
||||
command.enabled = groupSources.length > 0;
|
||||
// for (const source of groupSources) {
|
||||
// source.keyTextFromGroup = command.keyText;
|
||||
// }
|
||||
}
|
||||
}
|
||||
return res || dct;
|
||||
});
|
||||
}
|
||||
|
||||
let isInvalidatedDefinitions = false;
|
||||
|
||||
export async function invalidateCommandDefinitions() {
|
||||
if (isInvalidatedDefinitions) return;
|
||||
isInvalidatedDefinitions = true;
|
||||
await tick();
|
||||
|
||||
isInvalidatedDefinitions = false;
|
||||
|
||||
commands.update(dct => {
|
||||
let res = { ...dct };
|
||||
const values = Object.values(res) as GlobalCommand[];
|
||||
// test enabled for group commands
|
||||
for (const command of values) {
|
||||
if (!command.isGroupCommand) continue;
|
||||
const groupSources = values.filter(x => x.group == command.group && !x.isGroupCommand);
|
||||
|
||||
for (const source of groupSources) {
|
||||
source.keyTextFromGroup = command.keyText;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
invalidateCommands();
|
||||
}
|
||||
43
packages/web/src/commands/registerCommand.ts
Normal file
43
packages/web/src/commands/registerCommand.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { commands } from '../stores';
|
||||
import { invalidateCommandDefinitions } from './invalidateCommands';
|
||||
|
||||
export interface SubCommand {
|
||||
text: string;
|
||||
onClick: Function;
|
||||
}
|
||||
|
||||
export interface GlobalCommand {
|
||||
id: string;
|
||||
category: string; // null for group commands
|
||||
isGroupCommand?: boolean;
|
||||
name: string;
|
||||
text?: string /* category: name */;
|
||||
keyText?: string;
|
||||
keyTextFromGroup?: string; // automatically filled from group
|
||||
group?: string;
|
||||
getSubCommands?: () => SubCommand[];
|
||||
onClick?: Function;
|
||||
testEnabled?: () => boolean;
|
||||
// enabledStore?: any;
|
||||
icon?: string;
|
||||
toolbar?: boolean;
|
||||
enabled?: boolean;
|
||||
showDisabled?: boolean;
|
||||
toolbarName?: string;
|
||||
menuName?: string;
|
||||
toolbarOrder?: number;
|
||||
disableHandleKeyText?: string;
|
||||
}
|
||||
|
||||
export default function registerCommand(command: GlobalCommand) {
|
||||
const { testEnabled } = command;
|
||||
commands.update(x => ({
|
||||
...x,
|
||||
[command.id]: {
|
||||
text: `${command.category}: ${command.name}`,
|
||||
...command,
|
||||
enabled: !testEnabled,
|
||||
},
|
||||
}));
|
||||
invalidateCommandDefinitions();
|
||||
}
|
||||
26
packages/web/src/commands/runCommand.ts
Normal file
26
packages/web/src/commands/runCommand.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getCommands } from '../stores';
|
||||
import { GlobalCommand } from './registerCommand';
|
||||
|
||||
export default function runCommand(id) {
|
||||
const commandsValue = getCommands();
|
||||
const command = commandsValue[id];
|
||||
if (command) {
|
||||
if (!command.enabled) return;
|
||||
if (command.isGroupCommand) {
|
||||
runGroupCommand(command.group);
|
||||
} else {
|
||||
if (command.onClick) {
|
||||
command.onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window['dbgate_runCommand'] = runCommand;
|
||||
|
||||
export function runGroupCommand(group) {
|
||||
const commandsValue = getCommands();
|
||||
const values = Object.values(commandsValue) as GlobalCommand[];
|
||||
const real = values.find(x => x.group == group && !x.isGroupCommand && x.enabled);
|
||||
if (real && real.onClick) real.onClick();
|
||||
}
|
||||
291
packages/web/src/commands/stdCommands.ts
Normal file
291
packages/web/src/commands/stdCommands.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { currentTheme, extensions, getVisibleToolbar, visibleToolbar } from '../stores';
|
||||
import registerCommand from './registerCommand';
|
||||
import { derived, get } from 'svelte/store';
|
||||
import { ThemeDefinition } from 'dbgate-types';
|
||||
import ConnectionModal from '../modals/ConnectionModal.svelte';
|
||||
import AboutModal from '../modals/AboutModal.svelte';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import newQuery from '../query/newQuery';
|
||||
import saveTabFile from '../utility/saveTabFile';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { openElectronFile } from '../utility/openElectronFile';
|
||||
import { getDefaultFileFormat } from '../plugins/fileformats';
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
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),
|
||||
testEnabled: () => !getVisibleToolbar(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'toolbar.hide',
|
||||
category: 'Toolbar',
|
||||
name: 'Hide',
|
||||
onClick: () => visibleToolbar.set(0),
|
||||
testEnabled: () => getVisibleToolbar(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'about.show',
|
||||
category: 'About',
|
||||
name: 'Show',
|
||||
toolbarName: 'About',
|
||||
onClick: () => showModal(AboutModal),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.connection',
|
||||
toolbar: true,
|
||||
icon: 'icon connection',
|
||||
toolbarName: 'Add connection',
|
||||
category: 'New',
|
||||
toolbarOrder: 1,
|
||||
name: 'Connection',
|
||||
onClick: () => showModal(ConnectionModal),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.query',
|
||||
category: 'New',
|
||||
icon: 'icon file',
|
||||
toolbar: true,
|
||||
toolbarOrder: 2,
|
||||
name: 'Query',
|
||||
keyText: 'Ctrl+Q',
|
||||
onClick: () => newQuery(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.shell',
|
||||
category: 'New',
|
||||
icon: 'img shell',
|
||||
name: 'JavaScript Shell',
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Shell #',
|
||||
icon: 'img shell',
|
||||
tabComponent: 'ShellTab',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.markdown',
|
||||
category: 'New',
|
||||
icon: 'img markdown',
|
||||
name: 'Markdown page',
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Page #',
|
||||
icon: 'img markdown',
|
||||
tabComponent: 'MarkdownEditorTab',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.freetable',
|
||||
category: 'New',
|
||||
icon: 'img markdown',
|
||||
name: 'Free table editor',
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Data #',
|
||||
icon: 'img free-table',
|
||||
tabComponent: 'FreeTableTab',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'group.save',
|
||||
category: null,
|
||||
isGroupCommand: true,
|
||||
name: 'Save',
|
||||
keyText: 'Ctrl+S',
|
||||
group: 'save',
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'group.saveAs',
|
||||
category: null,
|
||||
isGroupCommand: true,
|
||||
name: 'Save As',
|
||||
keyText: 'Ctrl+Shift+S',
|
||||
group: 'saveAs',
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'group.undo',
|
||||
category: null,
|
||||
isGroupCommand: true,
|
||||
name: 'Undo',
|
||||
keyText: 'Ctrl+Z',
|
||||
group: 'undo',
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'group.redo',
|
||||
category: null,
|
||||
isGroupCommand: true,
|
||||
name: 'Redo',
|
||||
keyText: 'Ctrl+Y',
|
||||
group: 'redo',
|
||||
});
|
||||
|
||||
if (electron) {
|
||||
registerCommand({
|
||||
id: 'file.open',
|
||||
category: 'File',
|
||||
name: 'Open',
|
||||
keyText: 'Ctrl+O',
|
||||
onClick: openElectronFile,
|
||||
});
|
||||
}
|
||||
|
||||
registerCommand({
|
||||
id: 'file.import',
|
||||
category: 'File',
|
||||
name: 'Import data',
|
||||
toolbar: true,
|
||||
icon: 'icon import',
|
||||
onClick: () =>
|
||||
showModal(ImportExportModal, {
|
||||
importToArchive: true,
|
||||
initialValues: { sourceStorageType: getDefaultFileFormat(get(extensions)).storageType },
|
||||
}),
|
||||
});
|
||||
|
||||
export function registerFileCommands({
|
||||
idPrefix,
|
||||
category,
|
||||
getCurrentEditor,
|
||||
folder,
|
||||
format,
|
||||
fileExtension,
|
||||
save = true,
|
||||
execute = false,
|
||||
toggleComment = false,
|
||||
findReplace = false,
|
||||
undoRedo = false,
|
||||
}) {
|
||||
if (save) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.save',
|
||||
group: 'save',
|
||||
category,
|
||||
name: 'Save',
|
||||
// keyText: 'Ctrl+S',
|
||||
icon: 'icon save',
|
||||
toolbar: true,
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => saveTabFile(getCurrentEditor(), false, folder, format, fileExtension),
|
||||
});
|
||||
registerCommand({
|
||||
id: idPrefix + '.saveAs',
|
||||
group: 'saveAs',
|
||||
category,
|
||||
name: 'Save As',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => saveTabFile(getCurrentEditor(), true, folder, format, fileExtension),
|
||||
});
|
||||
}
|
||||
|
||||
if (execute) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.execute',
|
||||
category,
|
||||
name: 'Execute',
|
||||
icon: 'icon run',
|
||||
toolbar: true,
|
||||
keyText: 'F5 | Ctrl+Enter',
|
||||
testEnabled: () => getCurrentEditor() != null && !getCurrentEditor()?.isBusy(),
|
||||
onClick: () => getCurrentEditor().execute(),
|
||||
});
|
||||
registerCommand({
|
||||
id: idPrefix + '.kill',
|
||||
category,
|
||||
name: 'Kill',
|
||||
icon: 'icon close',
|
||||
toolbar: true,
|
||||
testEnabled: () => getCurrentEditor() != null && getCurrentEditor()?.canKill(),
|
||||
onClick: () => getCurrentEditor().kill(),
|
||||
});
|
||||
}
|
||||
|
||||
if (toggleComment) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.toggleComment',
|
||||
category,
|
||||
name: 'Toggle comment',
|
||||
keyText: 'Ctrl+/',
|
||||
disableHandleKeyText: 'Ctrl+/',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().toggleComment(),
|
||||
});
|
||||
}
|
||||
|
||||
if (findReplace) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.find',
|
||||
category,
|
||||
name: 'Find',
|
||||
keyText: 'Ctrl+F',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().find(),
|
||||
});
|
||||
registerCommand({
|
||||
id: idPrefix + '.replace',
|
||||
category,
|
||||
keyText: 'Ctrl+H',
|
||||
name: 'Replace',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().replace(),
|
||||
});
|
||||
}
|
||||
if (undoRedo) {
|
||||
registerCommand({
|
||||
id: idPrefix + '.undo',
|
||||
category,
|
||||
name: 'Undo',
|
||||
group: 'undo',
|
||||
testEnabled: () => getCurrentEditor()?.canUndo(),
|
||||
onClick: () => getCurrentEditor().undo(),
|
||||
});
|
||||
registerCommand({
|
||||
id: idPrefix + '.redo',
|
||||
category,
|
||||
group: 'redo',
|
||||
name: 'Redo',
|
||||
testEnabled: () => getCurrentEditor()?.canRedo(),
|
||||
onClick: () => getCurrentEditor().redo(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -143,10 +143,10 @@ export default class ChangeSetGrider extends Grider {
|
||||
this.dispatchChangeSet({ type: 'redo' });
|
||||
}
|
||||
get canUndo() {
|
||||
return this.changeSetState.canUndo;
|
||||
return this.changeSetState?.canUndo;
|
||||
}
|
||||
get canRedo() {
|
||||
return this.changeSetState.canRedo;
|
||||
return this.changeSetState?.canRedo;
|
||||
}
|
||||
get containsChanges() {
|
||||
return changeSetContainsChanges(this.changeSet);
|
||||
@@ -155,10 +155,10 @@ export default class ChangeSetGrider extends Grider {
|
||||
return this.insertedRows.length > 0;
|
||||
}
|
||||
|
||||
static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
||||
return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
||||
}
|
||||
static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
||||
return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
|
||||
}
|
||||
// static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
||||
// return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
||||
// }
|
||||
// static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
||||
// return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ColumnLabel from './ColumnLabel';
|
||||
import DropDownButton from '../widgets/DropDownButton';
|
||||
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
||||
import { useSplitterDrag } from '../widgets/Splitter';
|
||||
import { isTypeDateTime } from 'dbgate-tools';
|
||||
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
import { FontIcon } from '../icons';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
|
||||
const HeaderDiv = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
`;
|
||||
|
||||
const LabelDiv = styled.div`
|
||||
flex: 1;
|
||||
min-width: 10px;
|
||||
// padding-left: 2px;
|
||||
padding: 2px;
|
||||
margin: auto;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const IconWrapper = styled.span`
|
||||
margin-left: 3px;
|
||||
`;
|
||||
|
||||
const ResizeHandle = styled.div`
|
||||
background-color: ${props => props.theme.border};
|
||||
width: 2px;
|
||||
cursor: col-resize;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const GroupingLabel = styled.span`
|
||||
color: green;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export default function ColumnHeaderControl({
|
||||
column,
|
||||
setSort,
|
||||
onResize,
|
||||
order,
|
||||
setGrouping,
|
||||
grouping,
|
||||
conid,
|
||||
database,
|
||||
}) {
|
||||
const onResizeDown = useSplitterDrag('clientX', onResize);
|
||||
const { foreignKey } = column;
|
||||
const openNewTab = useOpenNewTab();
|
||||
const theme = useTheme();
|
||||
|
||||
const openReferencedTable = () => {
|
||||
openDatabaseObjectDetail(openNewTab, 'TableDataTab', null, {
|
||||
schemaName: foreignKey.refSchemaName,
|
||||
pureName: foreignKey.refTableName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField: 'tables',
|
||||
});
|
||||
// openNewTab(setOpenedTabs, {
|
||||
// title: foreignKey.refTableName,
|
||||
// tooltip,
|
||||
// icon: sqlTemplate ? 'sql.svg' : icons[objectTypeField],
|
||||
// tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
|
||||
// props: {
|
||||
// schemaName,
|
||||
// pureName,
|
||||
// conid,
|
||||
// database,
|
||||
// objectTypeField,
|
||||
// initialArgs: sqlTemplate ? { sqlTemplate } : null,
|
||||
// },
|
||||
// });
|
||||
};
|
||||
return (
|
||||
<HeaderDiv>
|
||||
<LabelDiv>
|
||||
{grouping && (
|
||||
<GroupingLabel>{grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}:</GroupingLabel>
|
||||
)}
|
||||
|
||||
<ColumnLabel {...column} />
|
||||
{order == 'ASC' && (
|
||||
<IconWrapper>
|
||||
<FontIcon icon="img sort-asc" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
{order == 'DESC' && (
|
||||
<IconWrapper>
|
||||
<FontIcon icon="img sort-desc" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
</LabelDiv>
|
||||
{setSort && (
|
||||
<DropDownButton>
|
||||
<DropDownMenuItem onClick={() => setSort('ASC')}>Sort ascending</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setSort('DESC')}>Sort descending</DropDownMenuItem>
|
||||
<DropDownMenuDivider />
|
||||
{foreignKey && (
|
||||
<DropDownMenuItem onClick={openReferencedTable}>
|
||||
Open table <strong>{foreignKey.refTableName}</strong>
|
||||
</DropDownMenuItem>
|
||||
)}
|
||||
{foreignKey && <DropDownMenuDivider />}
|
||||
<DropDownMenuItem onClick={() => setGrouping('GROUP')}>Group by</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setGrouping('MAX')}>MAX</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setGrouping('MIN')}>MIN</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setGrouping('SUM')}>SUM</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setGrouping('AVG')}>AVG</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setGrouping('COUNT')}>COUNT</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setGrouping('COUNT DISTINCT')}>COUNT DISTINCT</DropDownMenuItem>
|
||||
{isTypeDateTime(column.dataType) && (
|
||||
<>
|
||||
<DropDownMenuDivider />
|
||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:YEAR')}>Group by YEAR</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:MONTH')}>Group by MONTH</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:DAY')}>Group by DAY</DropDownMenuItem>
|
||||
{/* <DropDownMenuItem onClick={() => setGrouping('GROUP:HOUR')}>Group by HOUR</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:MINUTE')}>Group by MINUTE</DropDownMenuItem> */}
|
||||
</>
|
||||
)}
|
||||
</DropDownButton>
|
||||
)}
|
||||
<ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} theme={theme} />
|
||||
</HeaderDiv>
|
||||
);
|
||||
}
|
||||
104
packages/web/src/datagrid/ColumnHeaderControl.svelte
Normal file
104
packages/web/src/datagrid/ColumnHeaderControl.svelte
Normal file
@@ -0,0 +1,104 @@
|
||||
<script>
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import DropDownButton from '../elements/DropDownButton.svelte';
|
||||
import splitterDrag from '../utility/splitterDrag';
|
||||
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
import { isTypeDateTime } from 'dbgate-tools';
|
||||
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject.svelte';
|
||||
|
||||
export let column;
|
||||
export let conid = undefined;
|
||||
export let database = undefined;
|
||||
export let setSort;
|
||||
export let grouping = undefined;
|
||||
export let order = undefined;
|
||||
export let setGrouping;
|
||||
|
||||
const openReferencedTable = () => {
|
||||
openDatabaseObjectDetail('TableDataTab', null, {
|
||||
schemaName: column.foreignKey.refSchemaName,
|
||||
pureName: column.foreignKey.refTableName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField: 'tables',
|
||||
});
|
||||
};
|
||||
|
||||
function getMenu() {
|
||||
return [
|
||||
{ onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||
{ onClick: () => setSort('DESC'), text: 'Sort descending' },
|
||||
|
||||
column.foreignKey && [{ divider: true }, { onClick: openReferencedTable, text: column.foreignKey.refTableName }],
|
||||
|
||||
{ divider: true },
|
||||
{ onClick: () => setGrouping('GROUP'), text: 'Group by' },
|
||||
{ onClick: () => setGrouping('MAX'), text: 'MAX' },
|
||||
{ onClick: () => setGrouping('MIN'), text: 'MIN' },
|
||||
{ onClick: () => setGrouping('SUM'), text: 'SUM' },
|
||||
{ onClick: () => setGrouping('AVG'), text: 'AVG' },
|
||||
{ onClick: () => setGrouping('COUNT'), text: 'COUNT' },
|
||||
{ onClick: () => setGrouping('COUNT DISTINCT'), text: 'COUNT DISTINCT' },
|
||||
|
||||
isTypeDateTime(column.dataType) && [
|
||||
{ divider: true },
|
||||
{ onClick: () => setGrouping('GROUP:YEAR'), text: 'Group by YEAR' },
|
||||
{ onClick: () => setGrouping('GROUP:MONTH'), text: 'Group by MONTH' },
|
||||
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
|
||||
],
|
||||
];
|
||||
}
|
||||
</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 menu={getMenu} />
|
||||
<div class="horizontal-split-handle resizeHandleControl" use:splitterDrag={'clientX'} on:resizeSplitter />
|
||||
</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;
|
||||
align-self: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
/* .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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
28
packages/web/src/datagrid/ColumnManager.svelte
Normal file
28
packages/web/src/datagrid/ColumnManager.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { filterName, GridDisplay } from 'dbgate-datalib';
|
||||
|
||||
import InlineButton from '../elements/InlineButton.svelte';
|
||||
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
|
||||
|
||||
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||
import SearchInput from '../elements/SearchInput.svelte';
|
||||
import ColumnManagerRow from './ColumnManagerRow.svelte';
|
||||
|
||||
export let managerSize;
|
||||
export let display: GridDisplay;
|
||||
|
||||
let filter;
|
||||
</script>
|
||||
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search columns" bind:value={filter} />
|
||||
<InlineButton on:click={() => display.hideAllColumns()}>Hide</InlineButton>
|
||||
<InlineButton on:click={() => display.showAllColumns()}>Show</InlineButton>
|
||||
</SearchBoxWrapper>
|
||||
<ManagerInnerContainer width={managerSize}>
|
||||
{#each display
|
||||
.getColumns(filter)
|
||||
.filter(column => filterName(filter, column.columnName)) as column (column.uniqueName)}
|
||||
<ColumnManagerRow {display} {column} />
|
||||
{/each}
|
||||
</ManagerInnerContainer>
|
||||
44
packages/web/src/datagrid/ColumnManagerRow.svelte
Normal file
44
packages/web/src/datagrid/ColumnManagerRow.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { plusExpandIcon } from '../icons/expandIcons';
|
||||
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
|
||||
export let column;
|
||||
export let display;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="row"
|
||||
on:click={e => {
|
||||
// @ts-ignore
|
||||
if (e.target.closest('.expandColumnIcon')) return;
|
||||
display.focusColumn(column.uniqueName);
|
||||
}}
|
||||
>
|
||||
<span class="expandColumnIcon">
|
||||
<FontIcon
|
||||
icon={column.foreignKey ? plusExpandIcon(display.isExpandedColumn(column.uniqueName)) : 'icon invisible-box'}
|
||||
on:click={() => display.toggleExpandedColumn(column.uniqueName)}
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={`margin-left: ${5 + (column.uniquePath.length - 1) * 10}px`}
|
||||
checked={column.isChecked}
|
||||
on:change={() => display.setColumnVisibility(column.uniquePath, !column.isChecked)}
|
||||
/>
|
||||
<ColumnLabel {...column} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.row {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.row:hover {
|
||||
background: var(--theme-bg-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -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();
|
||||
// }
|
||||
// }
|
||||
218
packages/web/src/datagrid/DataFilterControl.svelte
Normal file
218
packages/web/src/datagrid/DataFilterControl.svelte
Normal file
@@ -0,0 +1,218 @@
|
||||
<script context="module">
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { createMultiLineFilter, parseFilter } from 'dbgate-filterparser';
|
||||
import splitterDrag from '../utility/splitterDrag';
|
||||
|
||||
import FilterMultipleValuesModal from '../modals/FilterMultipleValuesModal.svelte';
|
||||
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import SetFilterModal from '../modals/SetFilterModal.svelte';
|
||||
import keycodes from '../utility/keycodes';
|
||||
|
||||
import DropDownButton from '../elements/DropDownButton.svelte';
|
||||
|
||||
export let isReadOnly = false;
|
||||
export let filterType;
|
||||
export let filter;
|
||||
export let setFilter;
|
||||
export let showResizeSplitter = false;
|
||||
|
||||
let value;
|
||||
let isError;
|
||||
let isOk;
|
||||
|
||||
function openFilterWindow(condition1) {
|
||||
showModal(SetFilterModal, { condition1, filterType, onFilter: setFilter });
|
||||
}
|
||||
|
||||
const filterMultipleValues = () => {
|
||||
showModal(FilterMultipleValuesModal, {
|
||||
onFilter: (mode, text) => setFilter(createMultiLineFilter(mode, text)),
|
||||
});
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
switch (filterType) {
|
||||
case 'number':
|
||||
return [
|
||||
{ onClick: () => setFilter(''), text: 'Clear Filter' },
|
||||
{ onClick: () => filterMultipleValues(), text: 'Filter multiple values' },
|
||||
{ onClick: () => openFilterWindow('='), text: 'Equals...' },
|
||||
{ onClick: () => openFilterWindow('['), text: 'Does Not Equal...' },
|
||||
{ onClick: () => setFilter('NULL'), text: 'Is Null' },
|
||||
{ onClick: () => setFilter('NOT NULL'), text: 'Is Not Null' },
|
||||
{ onClick: () => openFilterWindow('>'), text: 'Greater Than...' },
|
||||
{ onClick: () => openFilterWindow('>='), text: 'Greater Than Or Equal To...' },
|
||||
{ onClick: () => openFilterWindow('<'), text: 'Less Than...' },
|
||||
{ onClick: () => openFilterWindow('<='), text: 'Less Than Or Equal To...' },
|
||||
];
|
||||
case 'logical':
|
||||
return [
|
||||
{ onClick: () => setFilter(''), text: 'Clear Filter' },
|
||||
{ onClick: () => filterMultipleValues(), text: 'Filter multiple values' },
|
||||
{ onClick: () => setFilter('NULL'), text: 'Is Null' },
|
||||
{ onClick: () => setFilter('NOT NULL'), text: 'Is Not Null' },
|
||||
{ onClick: () => setFilter('TRUE'), text: 'Is True' },
|
||||
{ onClick: () => setFilter('FALSE'), text: 'Is False' },
|
||||
{ onClick: () => setFilter('TRUE, NULL'), text: 'Is True or NULL' },
|
||||
{ onClick: () => setFilter('FALSE, NULL'), text: 'Is False or NULL' },
|
||||
];
|
||||
case 'datetime':
|
||||
return [
|
||||
{ onClick: () => setFilter(''), text: 'Clear Filter' },
|
||||
{ onClick: () => filterMultipleValues(), text: 'Filter multiple values' },
|
||||
{ onClick: () => setFilter('NULL'), text: 'Is Null' },
|
||||
{ onClick: () => setFilter('NOT NULL'), text: 'Is Not Null' },
|
||||
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => openFilterWindow('<='), text: 'Before...' },
|
||||
{ onClick: () => openFilterWindow('>='), text: 'After...' },
|
||||
{ onClick: () => openFilterWindow('>=;<='), text: 'Between...' },
|
||||
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => setFilter('TOMORROW'), text: 'Tomorrow' },
|
||||
{ onClick: () => setFilter('TODAY'), text: 'Today' },
|
||||
{ onClick: () => setFilter('YESTERDAY'), text: 'Yesterday' },
|
||||
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => setFilter('NEXT WEEK'), text: 'Next Week' },
|
||||
{ onClick: () => setFilter('THIS WEEK'), text: 'This Week' },
|
||||
{ onClick: () => setFilter('LAST WEEK'), text: 'Last Week' },
|
||||
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => setFilter('NEXT MONTH'), text: 'Next Month' },
|
||||
{ onClick: () => setFilter('THIS MONTH'), text: 'This Month' },
|
||||
{ onClick: () => setFilter('LAST MONTH'), text: 'Last Month' },
|
||||
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => setFilter('NEXT YEAR'), text: 'Next Year' },
|
||||
{ onClick: () => setFilter('THIS YEAR'), text: 'This Year' },
|
||||
{ onClick: () => setFilter('LAST YEAR'), text: 'Last Year' },
|
||||
|
||||
{ divider: true },
|
||||
];
|
||||
case 'string':
|
||||
return [
|
||||
{ onClick: () => setFilter(''), text: 'Clear Filter' },
|
||||
{ onClick: () => filterMultipleValues(), text: 'Filter multiple values' },
|
||||
|
||||
{ onClick: () => openFilterWindow('='), text: 'Equals...' },
|
||||
{ onClick: () => openFilterWindow('['), text: 'Does Not Equal...' },
|
||||
{ onClick: () => setFilter('NULL'), text: 'Is Null' },
|
||||
{ onClick: () => setFilter('NOT NULL'), text: 'Is Not Null' },
|
||||
{ onClick: () => setFilter('EMPTY, NULL'), text: 'Is Empty Or Null' },
|
||||
{ onClick: () => setFilter('NOT EMPTY NOT NULL'), text: 'Has Not Empty Value' },
|
||||
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => openFilterWindow('+'), text: 'Contains...' },
|
||||
{ onClick: () => openFilterWindow('~'), text: 'Does Not Contain...' },
|
||||
{ onClick: () => openFilterWindow('^'), text: 'Begins With...' },
|
||||
{ onClick: () => openFilterWindow('!^'), text: 'Does Not Begin With...' },
|
||||
{ onClick: () => openFilterWindow('$'), text: 'Ends With...' },
|
||||
{ onClick: () => openFilterWindow('!$'), text: 'Does Not End With...' },
|
||||
];
|
||||
}
|
||||
|
||||
// return [
|
||||
// { text: 'Clear filter', onClick: () => (value = '') },
|
||||
// { text: 'Is Null', onClick: () => (value = 'NULL') },
|
||||
// { text: 'Is Not Null', onClick: () => (value = 'NOT NULL') },
|
||||
// ];
|
||||
}
|
||||
|
||||
const handleKeyDown = ev => {
|
||||
if (isReadOnly) return;
|
||||
if (ev.keyCode == keycodes.enter) {
|
||||
applyFilter();
|
||||
}
|
||||
if (ev.keyCode == keycodes.escape) {
|
||||
setFilter('');
|
||||
}
|
||||
// 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);
|
||||
// }
|
||||
};
|
||||
|
||||
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();
|
||||
setFilter(createMultiLineFilter('is', pastedText));
|
||||
}
|
||||
}
|
||||
|
||||
$: value = filter;
|
||||
|
||||
$: {
|
||||
try {
|
||||
isOk = false;
|
||||
isError = false;
|
||||
if (value) {
|
||||
parseFilter(value, filterType);
|
||||
isOk = true;
|
||||
}
|
||||
} catch (err) {
|
||||
isError = true;
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
setFilter(value);
|
||||
}
|
||||
|
||||
// $: if (value != filter) setFilter(value);
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
readOnly={isReadOnly}
|
||||
bind:value
|
||||
on:keydown={handleKeyDown}
|
||||
on:blur={applyFilter}
|
||||
on:paste={handlePaste}
|
||||
class:isError
|
||||
class:isOk
|
||||
/>
|
||||
<DropDownButton icon="icon filter" menu={createMenu} />
|
||||
{#if showResizeSplitter}
|
||||
<div class="horizontal-split-handle resizeHandleControl" use:splitterDrag={'clientX'} on:resizeSplitter />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 10px;
|
||||
}
|
||||
|
||||
input.isError {
|
||||
background-color: var(--theme-bg-red);
|
||||
}
|
||||
|
||||
input.isOk {
|
||||
background-color: var(--theme-bg-green);
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
63
packages/web/src/datagrid/DataGrid.svelte
Normal file
63
packages/web/src/datagrid/DataGrid.svelte
Normal file
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
|
||||
import FormViewFilters from '../formview/FormViewFilters.svelte';
|
||||
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
|
||||
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
|
||||
import ColumnManager from './ColumnManager.svelte';
|
||||
import ReferenceManager from './ReferenceManager.svelte';
|
||||
|
||||
export let config;
|
||||
export let gridCoreComponent;
|
||||
export let formViewComponent;
|
||||
export let formDisplay;
|
||||
|
||||
export let isDetailView = false;
|
||||
export let showReferences = false;
|
||||
|
||||
let managerSize;
|
||||
|
||||
$: isFormView = !!(formDisplay && formDisplay.config && formDisplay.config.isFormView);
|
||||
</script>
|
||||
|
||||
<HorizontalSplitter initialValue="300px" bind:size={managerSize}>
|
||||
<div class="left" slot="1">
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Columns" name="columns" height={showReferences ? '40%' : '60%'} skip={isFormView}>
|
||||
<ColumnManager {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem title="Filters" name="filters" height="30%" skip={!isFormView}>
|
||||
<FormViewFilters {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem
|
||||
title="References"
|
||||
name="references"
|
||||
height="30%"
|
||||
collapsed={isDetailView}
|
||||
skip={!showReferences}
|
||||
>
|
||||
<ReferenceManager {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
</div>
|
||||
<svelte:fragment slot="2">
|
||||
{#if isFormView}
|
||||
<svelte:component this={formViewComponent} {...$$props} />
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={gridCoreComponent}
|
||||
{...$$props}
|
||||
formViewAvailable={!!formViewComponent && !!formDisplay}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</HorizontalSplitter>
|
||||
|
||||
<style>
|
||||
.left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
background-color: var(--theme-bg-0);
|
||||
}
|
||||
</style>
|
||||
153
packages/web/src/datagrid/DataGridCell.svelte
Normal file
153
packages/web/src/datagrid/DataGridCell.svelte
Normal file
@@ -0,0 +1,153 @@
|
||||
<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';
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
|
||||
export let rowIndex;
|
||||
export let col;
|
||||
export let rowData;
|
||||
export let colIndex = undefined;
|
||||
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;
|
||||
export let isFocusedColumn = false;
|
||||
export let domCell = undefined;
|
||||
export let hideContent = false;
|
||||
export let onSetFormView;
|
||||
|
||||
$: value = (rowData || {})[col.uniqueName];
|
||||
</script>
|
||||
|
||||
<td
|
||||
bind:this={domCell}
|
||||
data-row={rowIndex}
|
||||
data-col={colIndex == null ? col.colIndex : colIndex}
|
||||
class:isSelected
|
||||
class:isFrameSelected
|
||||
class:isModifiedRow
|
||||
class:isModifiedCell
|
||||
class:isInserted
|
||||
class:isDeleted
|
||||
class:isAutofillSelected
|
||||
class:isFocusedColumn
|
||||
style={`width:${col.width}px; min-width:${col.width}px; max-width:${col.width}px`}
|
||||
>
|
||||
{#if hideContent}
|
||||
<slot />
|
||||
{:else}
|
||||
{#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}
|
||||
|
||||
{#if col.foreignKey && rowData[col.uniqueName]}
|
||||
<ShowFormButton on:click={() => onSetFormView(rowData, col)} />
|
||||
{/if}
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<style>
|
||||
td {
|
||||
font-weight: normal;
|
||||
border: 1px solid var(--theme-border);
|
||||
padding: 2px;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
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.isFocusedColumn {
|
||||
background: var(--theme-bg-alt);
|
||||
}
|
||||
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%;
|
||||
}
|
||||
td.isSelected {
|
||||
background: var(--theme-bg-selected);
|
||||
}
|
||||
|
||||
.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
1086
packages/web/src/datagrid/DataGridCore.svelte
Normal file
1086
packages/web/src/datagrid/DataGridCore.svelte
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,333 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import InplaceEditor from './InplaceEditor';
|
||||
import { cellIsSelected } from './gridutil';
|
||||
import { isTypeLogical } from 'dbgate-tools';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import { FontIcon } from '../icons';
|
||||
|
||||
const TableBodyCell = styled.td`
|
||||
font-weight: normal;
|
||||
border: 1px solid ${props => props.theme.border};
|
||||
// border-collapse: collapse;
|
||||
padding: 2px;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
${props =>
|
||||
props.isSelected &&
|
||||
!props.isAutofillSelected &&
|
||||
!props.isFocusedColumn &&
|
||||
`
|
||||
background: initial;
|
||||
background-color: ${props.theme.gridbody_selection[4]};
|
||||
color: ${props.theme.gridbody_invfont1};`}
|
||||
|
||||
${props =>
|
||||
props.isFrameSelected &&
|
||||
`
|
||||
outline: 3px solid ${props.theme.gridbody_selection[4]};
|
||||
outline-offset: -3px;`}
|
||||
|
||||
${props =>
|
||||
props.isAutofillSelected &&
|
||||
!props.isFocusedColumn &&
|
||||
`
|
||||
outline: 3px solid ${props.theme.gridbody_selection[4]};
|
||||
outline-offset: -3px;`}
|
||||
|
||||
${props =>
|
||||
props.isModifiedRow &&
|
||||
!props.isInsertedRow &&
|
||||
!props.isSelected &&
|
||||
!props.isAutofillSelected &&
|
||||
!props.isModifiedCell &&
|
||||
!props.isFocusedColumn &&
|
||||
`
|
||||
background-color: ${props.theme.gridbody_background_gold[1]};`}
|
||||
${props =>
|
||||
!props.isSelected &&
|
||||
!props.isAutofillSelected &&
|
||||
!props.isInsertedRow &&
|
||||
!props.isFocusedColumn &&
|
||||
props.isModifiedCell &&
|
||||
`
|
||||
background-color: ${props.theme.gridbody_background_orange[1]};`}
|
||||
|
||||
${props =>
|
||||
!props.isSelected &&
|
||||
!props.isAutofillSelected &&
|
||||
!props.isFocusedColumn &&
|
||||
props.isInsertedRow &&
|
||||
`
|
||||
background-color: ${props.theme.gridbody_background_green[1]};`}
|
||||
|
||||
${props =>
|
||||
!props.isSelected &&
|
||||
!props.isAutofillSelected &&
|
||||
!props.isFocusedColumn &&
|
||||
props.isDeletedRow &&
|
||||
`
|
||||
background-color: ${props.theme.gridbody_background_volcano[1]};
|
||||
`}
|
||||
|
||||
${props =>
|
||||
props.isFocusedColumn &&
|
||||
`
|
||||
background-color: ${props.theme.gridbody_background_yellow[0]};
|
||||
`}
|
||||
|
||||
${props =>
|
||||
props.isDeletedRow &&
|
||||
`
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
||||
// from http://www.patternify.com/
|
||||
background-repeat: repeat-x;
|
||||
background-position: 50% 50%;`}
|
||||
`;
|
||||
|
||||
const HintSpan = styled.span`
|
||||
color: ${props => props.theme.gridbody_font3};
|
||||
margin-left: 5px;
|
||||
`;
|
||||
const NullSpan = styled.span`
|
||||
color: ${props => props.theme.gridbody_font3};
|
||||
font-style: italic;
|
||||
`;
|
||||
|
||||
const TableBodyRow = styled.tr`
|
||||
// height: 35px;
|
||||
background-color: ${props => props.theme.gridbody_background};
|
||||
&:nth-child(6n + 3) {
|
||||
background-color: ${props => props.theme.gridbody_background_alt2};
|
||||
}
|
||||
&:nth-child(6n + 6) {
|
||||
background-color: ${props => props.theme.gridbody_background_alt3};
|
||||
}
|
||||
`;
|
||||
|
||||
const TableHeaderCell = styled.td`
|
||||
border: 1px solid ${props => props.theme.border};
|
||||
text-align: left;
|
||||
padding: 2px;
|
||||
background-color: ${props => props.theme.gridheader_background};
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const AutoFillPoint = styled.div`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: ${props => props.theme.gridbody_selection[6]};
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
overflow: visible;
|
||||
cursor: crosshair;
|
||||
`;
|
||||
|
||||
export const ShowFormButton = styled.div`
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 1px;
|
||||
color: ${props => props.theme.gridbody_font3};
|
||||
background-color: ${props => props.theme.gridheader_background};
|
||||
border: 1px solid ${props => props.theme.gridheader_background};
|
||||
&:hover {
|
||||
color: ${props => props.theme.gridheader_font_hover};
|
||||
border: 1px solid ${props => props.theme.border};
|
||||
top: 1px;
|
||||
right: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
function makeBulletString(value) {
|
||||
return _.pad('', value.length, '•');
|
||||
}
|
||||
|
||||
function highlightSpecialCharacters(value) {
|
||||
value = value.replace(/\n/g, '↲');
|
||||
value = value.replace(/\r/g, '');
|
||||
value = value.replace(/^(\s+)/, makeBulletString);
|
||||
value = value.replace(/(\s+)$/, makeBulletString);
|
||||
value = value.replace(/(\s\s+)/g, makeBulletString);
|
||||
return value;
|
||||
}
|
||||
|
||||
const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
||||
|
||||
export function CellFormattedValue({ value, dataType, theme }) {
|
||||
if (value == null) return <NullSpan theme={theme}>(NULL)</NullSpan>;
|
||||
if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (value === true) return '1';
|
||||
if (value === false) return '0';
|
||||
if (_.isNumber(value)) {
|
||||
if (value >= 10000 || value <= -10000) {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
if (_.isString(value)) {
|
||||
if (dateTimeRegex.test(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
return highlightSpecialCharacters(value);
|
||||
}
|
||||
if (_.isPlainObject(value)) {
|
||||
if (_.isArray(value.data)) {
|
||||
if (value.data.length == 1 && isTypeLogical(dataType)) return value.data[0];
|
||||
return <NullSpan theme={theme}>({value.data.length} bytes)</NullSpan>;
|
||||
}
|
||||
return <NullSpan theme={theme}>(RAW)</NullSpan>;
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
function RowHeaderCell({ rowIndex, theme, onSetFormView, rowData }) {
|
||||
const [mouseIn, setMouseIn] = React.useState(false);
|
||||
|
||||
return (
|
||||
<TableHeaderCell
|
||||
data-row={rowIndex}
|
||||
data-col="header"
|
||||
theme={theme}
|
||||
onMouseEnter={onSetFormView ? () => setMouseIn(true) : null}
|
||||
onMouseLeave={onSetFormView ? () => setMouseIn(false) : null}
|
||||
>
|
||||
{rowIndex + 1}
|
||||
{!!onSetFormView && mouseIn && (
|
||||
<ShowFormButton
|
||||
theme={theme}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSetFormView(rowData);
|
||||
}}
|
||||
>
|
||||
<FontIcon icon="icon form" />
|
||||
</ShowFormButton>
|
||||
)}
|
||||
</TableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
function DataGridRow(props) {
|
||||
const {
|
||||
rowHeight,
|
||||
rowIndex,
|
||||
visibleRealColumns,
|
||||
inplaceEditorState,
|
||||
dispatchInsplaceEditor,
|
||||
autofillMarkerCell,
|
||||
selectedCells,
|
||||
autofillSelectedCells,
|
||||
focusedColumn,
|
||||
grider,
|
||||
frameSelection,
|
||||
onSetFormView,
|
||||
} = props;
|
||||
// usePropsCompare({
|
||||
// rowHeight,
|
||||
// rowIndex,
|
||||
// visibleRealColumns,
|
||||
// inplaceEditorState,
|
||||
// dispatchInsplaceEditor,
|
||||
// row,
|
||||
// display,
|
||||
// changeSet,
|
||||
// setChangeSet,
|
||||
// insertedRowIndex,
|
||||
// autofillMarkerCell,
|
||||
// selectedCells,
|
||||
// autofillSelectedCells,
|
||||
// });
|
||||
|
||||
// console.log('RENDER ROW', rowIndex);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const rowData = grider.getRowData(rowIndex);
|
||||
const rowStatus = grider.getRowStatus(rowIndex);
|
||||
|
||||
const hintFieldsAllowed = visibleRealColumns
|
||||
.filter(col => {
|
||||
if (!col.hintColumnName) return false;
|
||||
if (rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) return false;
|
||||
return true;
|
||||
})
|
||||
.map(col => col.uniqueName);
|
||||
|
||||
if (!rowData) return null;
|
||||
|
||||
return (
|
||||
<TableBodyRow style={{ height: `${rowHeight}px` }} theme={theme}>
|
||||
<RowHeaderCell rowIndex={rowIndex} theme={theme} onSetFormView={onSetFormView} rowData={rowData} />
|
||||
|
||||
{visibleRealColumns.map(col => (
|
||||
<TableBodyCell
|
||||
key={col.uniqueName}
|
||||
theme={theme}
|
||||
style={{
|
||||
width: col.widthPx,
|
||||
minWidth: col.widthPx,
|
||||
maxWidth: col.widthPx,
|
||||
}}
|
||||
data-row={rowIndex}
|
||||
data-col={col.colIndex}
|
||||
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
|
||||
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
|
||||
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
|
||||
isModifiedRow={rowStatus.status == 'updated'}
|
||||
isFocusedColumn={col.uniqueName == focusedColumn}
|
||||
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
||||
isInsertedRow={
|
||||
rowStatus.status == 'inserted' || (rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))
|
||||
}
|
||||
isDeletedRow={
|
||||
rowStatus.status == 'deleted' || (rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))
|
||||
}
|
||||
>
|
||||
{inplaceEditorState.cell &&
|
||||
rowIndex == inplaceEditorState.cell[0] &&
|
||||
col.colIndex == inplaceEditorState.cell[1] ? (
|
||||
<InplaceEditor
|
||||
widthPx={col.widthPx}
|
||||
inplaceEditorState={inplaceEditorState}
|
||||
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
||||
cellValue={rowData[col.uniqueName]}
|
||||
// grider={grider}
|
||||
// rowIndex={rowIndex}
|
||||
// uniqueName={col.uniqueName}
|
||||
onSetValue={value => grider.setCellValue(rowIndex, col.uniqueName, value)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<CellFormattedValue value={rowData[col.uniqueName]} dataType={col.dataType} theme={theme} />
|
||||
{hintFieldsAllowed.includes(col.uniqueName) && (
|
||||
<HintSpan theme={theme}>{rowData[col.hintColumnName]}</HintSpan>
|
||||
)}
|
||||
{col.foreignKey && rowData[col.uniqueName] && (
|
||||
<ShowFormButton
|
||||
theme={theme}
|
||||
className="buttonLike"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSetFormView(rowData, col);
|
||||
}}
|
||||
>
|
||||
<FontIcon icon="icon form" />
|
||||
</ShowFormButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{autofillMarkerCell && autofillMarkerCell[1] == col.colIndex && autofillMarkerCell[0] == rowIndex && (
|
||||
<AutoFillPoint className="autofillHandleMarker" theme={theme}></AutoFillPoint>
|
||||
)}
|
||||
</TableBodyCell>
|
||||
))}
|
||||
</TableBodyRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(DataGridRow);
|
||||
80
packages/web/src/datagrid/DataGridRow.svelte
Normal file
80
packages/web/src/datagrid/DataGridRow.svelte
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import openReferenceForm from '../formview/openReferenceForm';
|
||||
|
||||
import DataGridCell from './DataGridCell.svelte';
|
||||
import { cellIsSelected } from './gridutil';
|
||||
import InplaceEditor from './InplaceEditor.svelte';
|
||||
|
||||
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;
|
||||
export let focusedColumn = undefined;
|
||||
export let inplaceEditorState;
|
||||
export let dispatchInsplaceEditor;
|
||||
export let onSetFormView;
|
||||
|
||||
$: 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} onShowForm={onSetFormView ? () => onSetFormView(rowData, null) : null} />
|
||||
{#each visibleRealColumns as col (col.uniqueName)}
|
||||
{#if inplaceEditorState.cell && rowIndex == inplaceEditorState.cell[0] && col.colIndex == inplaceEditorState.cell[1]}
|
||||
<td>
|
||||
<InplaceEditor
|
||||
width={col.width}
|
||||
{inplaceEditorState}
|
||||
{dispatchInsplaceEditor}
|
||||
cellValue={rowData[col.uniqueName]}
|
||||
onSetValue={value => grider.setCellValue(rowIndex, col.uniqueName, value)}
|
||||
/>
|
||||
</td>
|
||||
{:else}
|
||||
<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)}
|
||||
isFocusedColumn={col.uniqueName == focusedColumn}
|
||||
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
||||
isModifiedRow={rowStatus.status == 'updated'}
|
||||
isInserted={rowStatus.status == 'inserted' ||
|
||||
(rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))}
|
||||
isDeleted={rowStatus.status == 'deleted' ||
|
||||
(rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))}
|
||||
{onSetFormView}
|
||||
/>
|
||||
{/if}
|
||||
{/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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
81
packages/web/src/datagrid/InplaceEditor.svelte
Normal file
81
packages/web/src/datagrid/InplaceEditor.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import keycodes from '../utility/keycodes';
|
||||
import { onMount } from 'svelte';
|
||||
import createRef from '../utility/createRef';
|
||||
|
||||
export let inplaceEditorState;
|
||||
export let dispatchInsplaceEditor;
|
||||
export let onSetValue;
|
||||
export let width;
|
||||
export let cellValue;
|
||||
|
||||
let domEditor;
|
||||
|
||||
const widthCopy = width;
|
||||
|
||||
const isChangedRef = createRef(!!inplaceEditorState.text);
|
||||
|
||||
function handleKeyDown(event) {
|
||||
switch (event.keyCode) {
|
||||
case keycodes.escape:
|
||||
isChangedRef.set(false);
|
||||
dispatchInsplaceEditor({ type: 'close' });
|
||||
break;
|
||||
case keycodes.enter:
|
||||
if (isChangedRef.get()) {
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
onSetValue(domEditor.value);
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
domEditor.blur();
|
||||
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
|
||||
break;
|
||||
case keycodes.s:
|
||||
if (event.ctrlKey) {
|
||||
if (isChangedRef.get()) {
|
||||
onSetValue(domEditor.value);
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
event.preventDefault();
|
||||
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
if (isChangedRef.get()) {
|
||||
onSetValue(domEditor.value);
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
dispatchInsplaceEditor({ type: 'close' });
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
domEditor.value = inplaceEditorState.text || cellValue;
|
||||
domEditor.focus();
|
||||
if (inplaceEditorState.selectAll) {
|
||||
domEditor.select();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
on:change={() => isChangedRef.set(true)}
|
||||
on:keydown={handleKeyDown}
|
||||
on:blur={handleBlur}
|
||||
bind:this={domEditor}
|
||||
style={widthCopy ? `width:${widthCopy}px;min-width:${widthCopy}px;max-width:${widthCopy}px` : undefined}
|
||||
/>
|
||||
|
||||
<style>
|
||||
input {
|
||||
border: 0px solid;
|
||||
outline: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
</style>
|
||||
29
packages/web/src/datagrid/JslDataGrid.svelte
Normal file
29
packages/web/src/datagrid/JslDataGrid.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { createGridCache, createGridConfig, JslGridDisplay } from 'dbgate-datalib';
|
||||
import { writable } from 'svelte/store';
|
||||
import socket from '../utility/socket';
|
||||
import useEffect from '../utility/useEffect';
|
||||
|
||||
import useFetch from '../utility/useFetch';
|
||||
import DataGrid from './DataGrid.svelte';
|
||||
import JslDataGridCore from './JslDataGridCore.svelte';
|
||||
|
||||
export let jslid;
|
||||
|
||||
$: info = useFetch({
|
||||
params: { jslid },
|
||||
url: 'jsldata/get-info',
|
||||
defaultValue: {},
|
||||
});
|
||||
|
||||
$: columns = ($info && $info.columns) || [];
|
||||
const config = writable(createGridConfig());
|
||||
const cache = writable(createGridCache());
|
||||
|
||||
$: display = new JslGridDisplay(jslid, columns, $config, config.update, $cache, cache.update);
|
||||
|
||||
</script>
|
||||
|
||||
{#key jslid}
|
||||
<DataGrid {display} {jslid} gridCoreComponent={JslDataGridCore} />
|
||||
{/key}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user