mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-30 01:03:58 +00:00
Merge branch 'develop'
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,6 +12,8 @@ node_modules
|
|||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
|
||||||
|
app/packages/web/public
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
@@ -24,3 +26,4 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
app/src/nativeModulesContent.js
|
app/src/nativeModulesContent.js
|
||||||
packages/api/src/nativeModulesContent.js
|
packages/api/src/nativeModulesContent.js
|
||||||
|
.VSCodeCounter
|
||||||
@@ -68,10 +68,10 @@
|
|||||||
"start": "cross-env ELECTRON_START_URL=http://localhost:5000 electron .",
|
"start": "cross-env ELECTRON_START_URL=http://localhost:5000 electron .",
|
||||||
"start:local": "cross-env electron .",
|
"start:local": "cross-env electron .",
|
||||||
"dist": "electron-builder",
|
"dist": "electron-builder",
|
||||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn dist",
|
"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:app && cd ../../app && yarn predist",
|
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"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",
|
"main": "src/electron.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ autoUpdater.logger = log;
|
|||||||
// TODO - create settings for this
|
// TODO - create settings for this
|
||||||
// appUpdater.channel = 'beta';
|
// appUpdater.channel = 'beta';
|
||||||
|
|
||||||
|
let commands = {};
|
||||||
|
|
||||||
function hideSplash() {
|
function hideSplash() {
|
||||||
if (splashWindow) {
|
if (splashWindow) {
|
||||||
splashWindow.destroy();
|
splashWindow.destroy();
|
||||||
@@ -35,61 +37,35 @@ function hideSplash() {
|
|||||||
mainWindow.show();
|
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() {
|
function buildMenu() {
|
||||||
const template = [
|
const template = [
|
||||||
{
|
{
|
||||||
label: 'File',
|
label: 'File',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
commandItem('new.connection'),
|
||||||
label: 'Connect to database',
|
commandItem('file.open'),
|
||||||
click() {
|
commandItem('group.save'),
|
||||||
mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`);
|
commandItem('group.saveAs'),
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'close' },
|
{ role: 'close' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Window',
|
label: 'Window',
|
||||||
submenu: [
|
submenu: [commandItem('new.query'), { type: 'separator' }, commandItem('tabs.closeAll'), { role: 'minimize' }],
|
||||||
{
|
|
||||||
label: 'New query',
|
|
||||||
click() {
|
|
||||||
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Close all tabs',
|
|
||||||
click() {
|
|
||||||
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ role: 'minimize' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
@@ -144,12 +120,7 @@ function buildMenu() {
|
|||||||
require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new');
|
require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
commandItem('about.show'),
|
||||||
label: 'About',
|
|
||||||
click() {
|
|
||||||
mainWindow.webContents.executeJavaScript(`dbgate_showAbout()`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -157,10 +128,22 @@ function buildMenu() {
|
|||||||
return Menu.buildFromTemplate(template);
|
return Menu.buildFromTemplate(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on('update-menu', async (event, arg) => {
|
ipcMain.on('update-commands', async (event, arg) => {
|
||||||
const commands = await mainWindow.webContents.executeJavaScript(`dbgate_getCurrentTabCommands()`);
|
commands = JSON.parse(arg);
|
||||||
mainMenu.getMenuItemById('save').enabled = !!commands.save;
|
for (const key of Object.keys(commands)) {
|
||||||
mainMenu.getMenuItemById('saveAs').enabled = !!commands.saveAs;
|
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() {
|
function createWindow() {
|
||||||
@@ -186,7 +169,7 @@ function createWindow() {
|
|||||||
const startUrl =
|
const startUrl =
|
||||||
process.env.ELECTRON_START_URL ||
|
process.env.ELECTRON_START_URL ||
|
||||||
url.format({
|
url.format({
|
||||||
pathname: path.join(__dirname, '../packages/web/build/index.html'),
|
pathname: path.join(__dirname, '../packages/web/public/index.html'),
|
||||||
protocol: 'file:',
|
protocol: 'file:',
|
||||||
slashes: true,
|
slashes: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"start:api": "yarn workspace dbgate-api start",
|
"start:api": "yarn workspace dbgate-api start",
|
||||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||||
"start:api:covid": "yarn workspace dbgate-api start:covid",
|
"start:api:covid": "yarn workspace dbgate-api start:covid",
|
||||||
"start:web": "yarn workspace dbgate-web start",
|
"start:web": "yarn workspace dbgate-web dev",
|
||||||
"start:sqltree": "yarn workspace dbgate-sqltree start",
|
"start:sqltree": "yarn workspace dbgate-sqltree start",
|
||||||
"start:tools": "yarn workspace dbgate-tools start",
|
"start:tools": "yarn workspace dbgate-tools start",
|
||||||
"start:datalib": "yarn workspace dbgate-datalib start",
|
"start:datalib": "yarn workspace dbgate-datalib start",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
"build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
||||||
"build:app": "cd app && yarn install && yarn build",
|
"build:app": "cd app && yarn install && yarn build",
|
||||||
"build:api": "yarn workspace dbgate-api 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",
|
"build:app:local": "cd app && yarn build:local",
|
||||||
"start:app:local": "cd app && yarn start:local",
|
"start:app:local": "cd app && yarn start:local",
|
||||||
"setCurrentVersion": "node setCurrentVersion",
|
"setCurrentVersion": "node setCurrentVersion",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"fillNativeModules": "node fillNativeModules",
|
"fillNativeModules": "node fillNativeModules",
|
||||||
"fillNativeModulesElectron": "node fillNativeModules --eletron",
|
"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",
|
"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:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build",
|
||||||
"prepare": "yarn build:lib",
|
"prepare": "yarn build:lib",
|
||||||
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",
|
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",
|
||||||
|
|||||||
@@ -121,7 +121,13 @@ module.exports = {
|
|||||||
getStats_meta: 'get',
|
getStats_meta: 'get',
|
||||||
getStats({ jslid }) {
|
getStats({ jslid }) {
|
||||||
const file = `${getJslFileName(jslid)}.stats`;
|
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 {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const hasPermission = require('../utility/hasPermission');
|
|||||||
|
|
||||||
const preinstallPluginMinimalVersions = {
|
const preinstallPluginMinimalVersions = {
|
||||||
'dbgate-plugin-mssql': '1.1.0',
|
'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-postgres': '1.1.0',
|
||||||
'dbgate-plugin-csv': '1.0.8',
|
'dbgate-plugin-csv': '1.0.8',
|
||||||
'dbgate-plugin-excel': '1.0.6',
|
'dbgate-plugin-excel': '1.0.6',
|
||||||
@@ -149,7 +149,7 @@ module.exports = {
|
|||||||
return content.commands[command](args);
|
return content.commands[command](args);
|
||||||
},
|
},
|
||||||
|
|
||||||
authTypes_meta: 'post',
|
authTypes_meta: 'get',
|
||||||
async authTypes({ engine }) {
|
async authTypes({ engine }) {
|
||||||
const packageName = extractPackageName(engine);
|
const packageName = extractPackageName(engine);
|
||||||
const content = requirePlugin(packageName);
|
const content = requirePlugin(packageName);
|
||||||
|
|||||||
@@ -347,5 +347,6 @@ export function changeSetInsertNewRow(changeSet: ChangeSet, name?: NamedObjectIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function changeSetContainsChanges(changeSet: ChangeSet) {
|
export function changeSetContainsChanges(changeSet: ChangeSet) {
|
||||||
|
if (!changeSet) return false;
|
||||||
return changeSet.deletes.length > 0 || changeSet.updates.length > 0 || changeSet.inserts.length > 0;
|
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 {
|
export interface EngineDriver {
|
||||||
engine: string;
|
engine: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
defaultPort?: number;
|
||||||
connect({ server, port, user, password, database }): any;
|
connect({ server, port, user, password, database }): any;
|
||||||
query(pool: any, sql: string): Promise<QueryResult>;
|
query(pool: any, sql: string): Promise<QueryResult>;
|
||||||
stream(pool: any, sql: string, options: StreamOptions);
|
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;
|
getOutputParams?: (sourceName, values) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ThemeDefinition {
|
||||||
|
className: string;
|
||||||
|
themeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginDefinition {
|
export interface PluginDefinition {
|
||||||
packageName: string;
|
packageName: string;
|
||||||
manifest: any;
|
manifest: any;
|
||||||
@@ -31,4 +36,5 @@ export interface ExtensionsDirectory {
|
|||||||
plugins: PluginDefinition[];
|
plugins: PluginDefinition[];
|
||||||
fileFormats: FileFormatDefinition[];
|
fileFormats: FileFormatDefinition[];
|
||||||
drivers: EngineDriver[];
|
drivers: EngineDriver[];
|
||||||
|
themes: ThemeDefinition[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended"
|
|
||||||
],
|
|
||||||
"globals": {
|
|
||||||
"Atomics": "readonly",
|
|
||||||
"SharedArrayBuffer": "readonly"
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"ecmaVersion": 2018,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"react/prop-types": "off",
|
|
||||||
"no-unused-vars": "warn"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
105
packages/web/README.md
Normal file
105
packages/web/README.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# svelte app
|
||||||
|
|
||||||
|
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
|
||||||
|
|
||||||
|
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx degit sveltejs/template svelte-app
|
||||||
|
cd svelte-app
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
|
||||||
|
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
Install the dependencies...
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd svelte-app
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
...then start [Rollup](https://rollupjs.org):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
|
||||||
|
|
||||||
|
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
|
||||||
|
|
||||||
|
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
|
||||||
|
|
||||||
|
## Building and running in production mode
|
||||||
|
|
||||||
|
To create an optimised version of the app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
|
||||||
|
|
||||||
|
|
||||||
|
## Single-page app mode
|
||||||
|
|
||||||
|
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
|
||||||
|
|
||||||
|
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
|
||||||
|
|
||||||
|
```js
|
||||||
|
"start": "sirv public --single"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using TypeScript
|
||||||
|
|
||||||
|
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/setupTypeScript.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Or remove the script via:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm scripts/setupTypeScript.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploying to the web
|
||||||
|
|
||||||
|
### With [Vercel](https://vercel.com)
|
||||||
|
|
||||||
|
Install `vercel` if you haven't already:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, from within your project folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd public
|
||||||
|
vercel deploy --name my-project
|
||||||
|
```
|
||||||
|
|
||||||
|
### With [surge](https://surge.sh/)
|
||||||
|
|
||||||
|
Install `surge` if you haven't already:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g surge
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, from within your project folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
surge public my-project.surge.sh
|
||||||
|
```
|
||||||
@@ -1,69 +1,51 @@
|
|||||||
{
|
{
|
||||||
"name": "dbgate-web",
|
"name": "dbgate-web",
|
||||||
"version": "3.9.5",
|
"version": "1.0.0",
|
||||||
"files": [
|
|
||||||
"build"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env BROWSER=none PORT=5000 react-scripts start",
|
"build": "cross-env API_URL=ORIGIN rollup -c",
|
||||||
"build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
"dev": "cross-env API_URL=http://localhost:3000 rollup -c -w",
|
||||||
"build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build",
|
"start": "sirv public",
|
||||||
"build": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
"validate": "svelte-check"
|
||||||
"prepublishOnly": "yarn build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"ts": "tsc"
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^16.9.17",
|
|
||||||
"@types/styled-components": "^4.4.2",
|
|
||||||
"dbgate-types": "^3.9.5",
|
|
||||||
"typescript": "^3.7.4",
|
|
||||||
"@ant-design/colors": "^5.0.0",
|
"@ant-design/colors": "^5.0.0",
|
||||||
"@mdi/font": "^5.8.55",
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||||
"@testing-library/react": "^9.3.2",
|
"@rollup/plugin-replace": "^2.4.1",
|
||||||
"@testing-library/user-event": "^7.1.2",
|
"@rollup/plugin-typescript": "^6.0.0",
|
||||||
|
"@tsconfig/svelte": "^1.0.0",
|
||||||
"ace-builds": "^1.4.8",
|
"ace-builds": "^1.4.8",
|
||||||
"axios": "^0.19.0",
|
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"compare-versions": "^3.6.0",
|
"cross-env": "^7.0.3",
|
||||||
"cross-env": "^6.0.3",
|
|
||||||
"dbgate-datalib": "^3.9.5",
|
"dbgate-datalib": "^3.9.5",
|
||||||
"dbgate-sqltree": "^3.9.5",
|
"dbgate-sqltree": "^3.9.5",
|
||||||
"dbgate-tools": "^3.9.5",
|
"dbgate-tools": "^3.9.5",
|
||||||
"eslint": "^6.8.0",
|
"dbgate-types": "^3.9.5",
|
||||||
"eslint-plugin-react": "^7.17.0",
|
|
||||||
"json-stable-stringify": "^1.0.1",
|
"json-stable-stringify": "^1.0.1",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
"markdown-to-jsx": "^7.1.0",
|
"lodash": "^4.17.15",
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"react": "^16.12.0",
|
"rollup": "^2.3.4",
|
||||||
"react-ace": "^8.0.0",
|
"rollup-plugin-copy": "^3.3.0",
|
||||||
"react-chartjs-2": "^2.11.1",
|
"rollup-plugin-css-only": "^3.1.0",
|
||||||
"react-dom": "^16.12.0",
|
"rollup-plugin-livereload": "^2.0.0",
|
||||||
"react-dropzone": "^11.2.3",
|
"rollup-plugin-svelte": "^7.0.0",
|
||||||
"react-helmet": "^6.1.0",
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
"react-json-view": "^1.19.1",
|
|
||||||
"react-modal": "^3.11.1",
|
|
||||||
"react-scripts": "3.3.0",
|
|
||||||
"react-select": "^3.1.0",
|
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
|
||||||
"socket.io-client": "^2.3.0",
|
"socket.io-client": "^2.3.0",
|
||||||
"sql-formatter": "^2.3.3",
|
"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"
|
"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">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset='utf-8'>
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
|
<title>DbGate</title>
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
|
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
<link rel='icon' type='image/png' href='favicon.ico'>
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
<link rel="manifest" href="manifest.json" />
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
<link rel='stylesheet' href='global.css'>
|
||||||
<title>DbGate</title>
|
<link rel='stylesheet' href='dimensions.css'>
|
||||||
|
<link rel='stylesheet' href='bulma.css'>
|
||||||
|
<link rel='stylesheet' href='icon-colors.css'>
|
||||||
|
<link rel='stylesheet' href='build/bundle.css'>
|
||||||
|
<link rel='stylesheet' href='build/fonts/materialdesignicons.css'>
|
||||||
|
|
||||||
|
<script defer src='build/bundle.js'></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root">Loading DbGate...</div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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 { dumpSqlSelect, Select } from 'dbgate-sqltree';
|
||||||
import { EngineDriver } from 'dbgate-types';
|
import { EngineDriver } from 'dbgate-types';
|
||||||
import axios from '../utility/axios';
|
import axiosInstance from '../utility/axiosInstance';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { extractDataColumns } from './DataChart';
|
|
||||||
|
|
||||||
export async function loadChartStructure(driver: EngineDriver, conid, database, sql) {
|
export async function loadChartStructure(driver: EngineDriver, conid, database, sql) {
|
||||||
const select: Select = {
|
const select: Select = {
|
||||||
@@ -17,7 +16,7 @@ export async function loadChartStructure(driver: EngineDriver, conid, database,
|
|||||||
|
|
||||||
const dmp = driver.createDumper();
|
const dmp = driver.createDumper();
|
||||||
dumpSqlSelect(dmp, select);
|
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);
|
if (resp.data.errorMessage) throw new Error(resp.data.errorMessage);
|
||||||
return resp.data.columns.map(x => x.columnName);
|
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();
|
const dmp = driver.createDumper();
|
||||||
dumpSqlSelect(dmp, select);
|
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;
|
let { rows, columns } = resp.data;
|
||||||
if (truncateFrom == 'end' && rows) {
|
if (truncateFrom == 'end' && rows) {
|
||||||
rows = _.reverse([...rows]);
|
rows = _.reverse([...rows]);
|
||||||
@@ -103,3 +102,21 @@ export async function loadChartData(driver: EngineDriver, conid, database, sql,
|
|||||||
rows,
|
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' });
|
this.dispatchChangeSet({ type: 'redo' });
|
||||||
}
|
}
|
||||||
get canUndo() {
|
get canUndo() {
|
||||||
return this.changeSetState.canUndo;
|
return this.changeSetState?.canUndo;
|
||||||
}
|
}
|
||||||
get canRedo() {
|
get canRedo() {
|
||||||
return this.changeSetState.canRedo;
|
return this.changeSetState?.canRedo;
|
||||||
}
|
}
|
||||||
get containsChanges() {
|
get containsChanges() {
|
||||||
return changeSetContainsChanges(this.changeSet);
|
return changeSetContainsChanges(this.changeSet);
|
||||||
@@ -155,10 +155,10 @@ export default class ChangeSetGrider extends Grider {
|
|||||||
return this.insertedRows.length > 0;
|
return this.insertedRows.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
// static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
||||||
return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
// return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
||||||
}
|
// }
|
||||||
static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
// static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
||||||
return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
|
// return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import ColumnLabel from './ColumnLabel';
|
|
||||||
import DropDownButton from '../widgets/DropDownButton';
|
|
||||||
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
|
||||||
import { useSplitterDrag } from '../widgets/Splitter';
|
|
||||||
import { isTypeDateTime } from 'dbgate-tools';
|
|
||||||
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject';
|
|
||||||
import { useSetOpenedTabs } from '../utility/globalState';
|
|
||||||
import { FontIcon } from '../icons';
|
|
||||||
import useTheme from '../theme/useTheme';
|
|
||||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
|
||||||
|
|
||||||
const HeaderDiv = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LabelDiv = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
min-width: 10px;
|
|
||||||
// padding-left: 2px;
|
|
||||||
padding: 2px;
|
|
||||||
margin: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const IconWrapper = styled.span`
|
|
||||||
margin-left: 3px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ResizeHandle = styled.div`
|
|
||||||
background-color: ${props => props.theme.border};
|
|
||||||
width: 2px;
|
|
||||||
cursor: col-resize;
|
|
||||||
z-index: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const GroupingLabel = styled.span`
|
|
||||||
color: green;
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function ColumnHeaderControl({
|
|
||||||
column,
|
|
||||||
setSort,
|
|
||||||
onResize,
|
|
||||||
order,
|
|
||||||
setGrouping,
|
|
||||||
grouping,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
}) {
|
|
||||||
const onResizeDown = useSplitterDrag('clientX', onResize);
|
|
||||||
const { foreignKey } = column;
|
|
||||||
const openNewTab = useOpenNewTab();
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const openReferencedTable = () => {
|
|
||||||
openDatabaseObjectDetail(openNewTab, 'TableDataTab', null, {
|
|
||||||
schemaName: foreignKey.refSchemaName,
|
|
||||||
pureName: foreignKey.refTableName,
|
|
||||||
conid,
|
|
||||||
database,
|
|
||||||
objectTypeField: 'tables',
|
|
||||||
});
|
|
||||||
// openNewTab(setOpenedTabs, {
|
|
||||||
// title: foreignKey.refTableName,
|
|
||||||
// tooltip,
|
|
||||||
// icon: sqlTemplate ? 'sql.svg' : icons[objectTypeField],
|
|
||||||
// tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
|
|
||||||
// props: {
|
|
||||||
// schemaName,
|
|
||||||
// pureName,
|
|
||||||
// conid,
|
|
||||||
// database,
|
|
||||||
// objectTypeField,
|
|
||||||
// initialArgs: sqlTemplate ? { sqlTemplate } : null,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<HeaderDiv>
|
|
||||||
<LabelDiv>
|
|
||||||
{grouping && (
|
|
||||||
<GroupingLabel>{grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}:</GroupingLabel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ColumnLabel {...column} />
|
|
||||||
{order == 'ASC' && (
|
|
||||||
<IconWrapper>
|
|
||||||
<FontIcon icon="img sort-asc" />
|
|
||||||
</IconWrapper>
|
|
||||||
)}
|
|
||||||
{order == 'DESC' && (
|
|
||||||
<IconWrapper>
|
|
||||||
<FontIcon icon="img sort-desc" />
|
|
||||||
</IconWrapper>
|
|
||||||
)}
|
|
||||||
</LabelDiv>
|
|
||||||
{setSort && (
|
|
||||||
<DropDownButton>
|
|
||||||
<DropDownMenuItem onClick={() => setSort('ASC')}>Sort ascending</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setSort('DESC')}>Sort descending</DropDownMenuItem>
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
{foreignKey && (
|
|
||||||
<DropDownMenuItem onClick={openReferencedTable}>
|
|
||||||
Open table <strong>{foreignKey.refTableName}</strong>
|
|
||||||
</DropDownMenuItem>
|
|
||||||
)}
|
|
||||||
{foreignKey && <DropDownMenuDivider />}
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP')}>Group by</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('MAX')}>MAX</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('MIN')}>MIN</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('SUM')}>SUM</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('AVG')}>AVG</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('COUNT')}>COUNT</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('COUNT DISTINCT')}>COUNT DISTINCT</DropDownMenuItem>
|
|
||||||
{isTypeDateTime(column.dataType) && (
|
|
||||||
<>
|
|
||||||
<DropDownMenuDivider />
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:YEAR')}>Group by YEAR</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:MONTH')}>Group by MONTH</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:DAY')}>Group by DAY</DropDownMenuItem>
|
|
||||||
{/* <DropDownMenuItem onClick={() => setGrouping('GROUP:HOUR')}>Group by HOUR</DropDownMenuItem>
|
|
||||||
<DropDownMenuItem onClick={() => setGrouping('GROUP:MINUTE')}>Group by MINUTE</DropDownMenuItem> */}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropDownButton>
|
|
||||||
)}
|
|
||||||
<ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} theme={theme} />
|
|
||||||
</HeaderDiv>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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