Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e78420b1b0 | |||
| 69d639b9ea | |||
| a540220c05 | |||
| b1ddcbfbfc | |||
| 87aaa281e4 | |||
| 704733d80d | |||
| a50458494e | |||
| 2a980a7892 | |||
| a3762c6caa | |||
| d6ba822338 | |||
| f146d70e2b | |||
| d62177d996 | |||
| a1993214e2 | |||
| 70411b764b | |||
| cd0fb0fdf2 | |||
| 9713c9b88e | |||
| d5118909d1 | |||
| bb41236a5f | |||
| 9d84c0f213 | |||
| d45fbcb8c8 | |||
| 4762597741 | |||
| 9c27c224ec | |||
| 2268d6126b | |||
| bbc50ea3fb | |||
| 11985004b5 | |||
| 4f58d2ff80 | |||
| 987fe6095a | |||
| d8fcbc8c17 | |||
| 833610c88b | |||
| 218478c128 | |||
| 3f8ff91e2c | |||
| 11436c065c | |||
| f1c70f6f82 | |||
| c66ff5820c | |||
| 4c87ad50b1 | |||
| 6c5f5a7cfb | |||
| 9876a76836 | |||
| 952bdc4baa | |||
| 2afa7a5f58 | |||
| bc9e8a2ea6 | |||
| e2dcfe9940 | |||
| 638b04877d | |||
| 2a9c67d2f6 | |||
| 2c9d424fc8 | |||
| 4e76f10175 | |||
| 5f2afc037e | |||
| 50e61cdce1 | |||
| 586f2fed21 | |||
| 020b4163d7 | |||
| 6b0e1e322a | |||
| 0d0bd29812 | |||
| 4e4447de8a | |||
| 15c9e93e8a | |||
| 5531705433 | |||
| dfadf0653d | |||
| 742b68453a | |||
| 155406827e | |||
| a97e6dbcab | |||
| d4989c75ca | |||
| b7b9dde5ae | |||
| 34f2fb2a0a | |||
| 965a967450 | |||
| 40872699c6 | |||
| ec90a8b952 | |||
| 90c4b44fdb | |||
| df5062c9a5 | |||
| 437155e4c5 | |||
| 5ebee161ae | |||
| 10c77ad153 | |||
| 5e59926556 | |||
| 4b1b61328a | |||
| 4c6d9f0660 | |||
| a5a7447cec | |||
| dd373f9db9 | |||
| bb0f5e4404 | |||
| c77bc820d4 | |||
| a1ab47a6f9 | |||
| efc07280a6 | |||
| dcb4c5071a | |||
| 7b625c6073 | |||
| 489c9a905c | |||
| 75c578de47 | |||
| f7c4bbc708 | |||
| 9c1227273c | |||
| 47a045fc24 | |||
| 9e9df60d37 | |||
| 93b7a9a674 | |||
| b96576ba6f | |||
| 0524b4c5b6 | |||
| b7663e2e06 | |||
| 24f4e1d898 | |||
| 9089f78593 | |||
| 8f90dad303 | |||
| c823e18699 | |||
| 73bfac2bfb | |||
| 08b5bce03c | |||
| ddf8a5806c | |||
| eceab2dde9 | |||
| 9c7df42948 | |||
| 321d5d71de | |||
| d4a35fb414 | |||
| 21feb3a042 | |||
| 9adb4b41c6 | |||
| 3b3e81e3f7 | |||
| dfa8ca6797 | |||
| 0af207d330 | |||
| 49337a4112 | |||
| 7cd26c4fe4 | |||
| 159ba72129 | |||
| cfb772c717 | |||
| 500c1c76ba | |||
| 834eeabd3f | |||
| 8770034bf5 | |||
| 423f876d68 | |||
| af54c958ba | |||
| 4e16119835 | |||
| d6f9db48aa | |||
| 2063005d5c | |||
| c2c54856ff | |||
| cedb740fb0 | |||
| 913f89e970 | |||
| 7d6bf90a0a | |||
| 8a4275fb09 | |||
| d5ebea3764 | |||
| a93aff1bb7 | |||
| c193955fbe | |||
| 5f97f7d922 | |||
| 54d17a67d4 | |||
| 0c94d7fcac | |||
| fca23f65de | |||
| a35d5525a3 | |||
| 904d35d26a | |||
| 929c08e094 | |||
| 97a27381f2 | |||
| f4fe5b9b53 | |||
| 00d5b25baa | |||
| c6e95dbb6a | |||
| da6bd9b475 | |||
| 2eebe44f87 | |||
| 3dd99a44cb | |||
| ec2acebdc9 | |||
| e49c0169da | |||
| 49f22e1a3b | |||
| 423644e9d9 | |||
| b64b6be68a | |||
| d3c4c18b62 | |||
| fe5826bc8e | |||
| 7709e24ccd | |||
| b91cf18aee | |||
| ae9c4b4f98 | |||
| 3d25a51cf9 | |||
| 18e7171038 | |||
| 8cf014efa4 | |||
| 78d71602bf | |||
| dcfd6ee1dc | |||
| eb4ecb4cf8 | |||
| bc54564d64 | |||
| 1c7052810a | |||
| 4bfba2ec02 | |||
| c2dc4d76ba | |||
| ce44e271ae | |||
| ef5bfb5a89 | |||
| 7acea0f4ac | |||
| 593e61abb9 | |||
| a0aa508e8d | |||
| ca517f9c73 | |||
| ad0e02de5d | |||
| ea05ae0079 | |||
| 689eb7baa2 | |||
| a4387155e7 | |||
| 565a60e638 | |||
| 5dba5a6dfd | |||
| c497c1ceca | |||
| 5929a01010 | |||
| 182b0e0a75 | |||
| 2cc74b594e | |||
| e9430988f4 | |||
| f30bd0a894 | |||
| 1c0a8cad56 | |||
| 8a4fd302d2 | |||
| db7f8d6a74 | |||
| 4f1eb4003a | |||
| 3efaac7d1f | |||
| d9387bef1f | |||
| a101f21483 | |||
| 8a0d10e50d | |||
| fe1fc7923f | |||
| f0802dc471 | |||
| 30ade5867c | |||
| ea54673497 | |||
| 2ffd729465 | |||
| ef910f43a6 | |||
| fc333167ac | |||
| 390447c948 | |||
| 1bb5f4974d | |||
| 48e3cf1be5 | |||
| 1e540b3fe9 | |||
| 60c1090d6c | |||
| 71bea87a7a | |||
| a03261bfd4 | |||
| 704a04e9bb | |||
| 28c1421294 | |||
| 7a5bcc62c8 | |||
| 321eedefea | |||
| daf9e9d18b | |||
| dd7db5904c | |||
| 6bddf3aa83 | |||
| 9743569ca7 | |||
| 40ed020c0a | |||
| 63ac08cc27 | |||
| e16b0ef61f | |||
| 14f9a40851 | |||
| ba6abd1e64 | |||
| 4ffc5842bb |
@@ -12,6 +12,8 @@ node_modules
|
||||
build
|
||||
dist
|
||||
|
||||
app/packages/web/public
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
@@ -24,3 +26,4 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
app/src/nativeModulesContent.js
|
||||
packages/api/src/nativeModulesContent.js
|
||||
.VSCodeCounter
|
||||
@@ -1,5 +1,16 @@
|
||||
# ChangeLog
|
||||
|
||||
### 4.0.0
|
||||
- CHANGED: Excahnged React with Svelte. Changed theme colors. Huge speed and memory optimalization
|
||||
- ADDED: SQL Generator (CREATE, INSERT, DROP)
|
||||
- ADDED: Command palette (F1). Introduced commands, extended some context menus
|
||||
- ADDED: New keyboard shortcuts
|
||||
- ADDED: Switch to recent database feature
|
||||
- ADDED: Macros from free table editor are available also in table data editor
|
||||
- CHANGED: Cell data preview is now in left widgets panel
|
||||
- CHANGED: Toolbar refactor
|
||||
- FIX: Solved reconnecting expired connection
|
||||
|
||||
### 3.9.6
|
||||
- ADDED: Connect using SSH Tunnel
|
||||
- ADDED: Connect using SSL
|
||||
|
||||
@@ -40,7 +40,7 @@ There are many database managers now, so why DbGate?
|
||||
## Design goals
|
||||
* Application simplicity - DbGate takes the best and only the best from old [DbGate](http://www.jenasoft.com/dbgate), [DatAdmin](http://www.jenasoft.com/datadmin) and [DbMouse](http://www.jenasoft.com/dbmouse) .
|
||||
* Minimal dependencies
|
||||
* Frontend - React, styled-components, socket.io
|
||||
* Frontend - Svelte, socket.io
|
||||
* Backend - NodeJs, ExpressJs, socket.io, database connection drivers
|
||||
* JavaScript + TypeScript
|
||||
* App - electron
|
||||
@@ -100,6 +100,6 @@ Some dbgate packages can be used also without DbGate. You can find them on [NPM
|
||||
* [filterparser](https://github.com/dbgate/dbgate/tree/master/packages/filterparser) - TypeScript library for parsing data filter expressions using parsimmon [](https://www.npmjs.com/package/dbgate-filterparser)
|
||||
* [sqltree](https://github.com/dbgate/dbgate/tree/master/packages/sqltree) - JSON representation of SQL query, functions converting to SQL (TypeScript) [](https://www.npmjs.com/package/dbgate-sqltree)
|
||||
* [types](https://github.com/dbgate/dbgate/tree/master/packages/types) - common TypeScript definitions [](https://www.npmjs.com/package/dbgate-types)
|
||||
* [web](https://github.com/dbgate/dbgate/tree/master/packages/web) - frontend in React (JavaScript) [](https://www.npmjs.com/package/dbgate-web)
|
||||
* [web](https://github.com/dbgate/dbgate/tree/master/packages/web) - frontend in Svelte (JavaScript) [](https://www.npmjs.com/package/dbgate-web)
|
||||
* [tools](https://github.com/dbgate/dbgate/tree/master/packages/tools) - various tools [](https://www.npmjs.com/package/dbgate-tools)
|
||||
|
||||
|
||||
+3
-3
@@ -68,10 +68,10 @@
|
||||
"start": "cross-env ELECTRON_START_URL=http://localhost:5000 electron .",
|
||||
"start:local": "cross-env electron .",
|
||||
"dist": "electron-builder",
|
||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn dist",
|
||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn predist",
|
||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
|
||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/build/*\" packages && copyfiles \"../packages/web/build/**/*\" packages"
|
||||
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/public/*\" packages && copyfiles \"../packages/web/public/**/*\" packages"
|
||||
},
|
||||
"main": "src/electron.js",
|
||||
"devDependencies": {
|
||||
|
||||
+39
-55
@@ -27,6 +27,8 @@ autoUpdater.logger = log;
|
||||
// TODO - create settings for this
|
||||
// appUpdater.channel = 'beta';
|
||||
|
||||
let commands = {};
|
||||
|
||||
function hideSplash() {
|
||||
if (splashWindow) {
|
||||
splashWindow.destroy();
|
||||
@@ -35,61 +37,35 @@ function hideSplash() {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
function commandItem(id) {
|
||||
const command = commands[id];
|
||||
return {
|
||||
id,
|
||||
label: command ? command.menuName || command.toolbarName || command.name : id,
|
||||
accelerator: command ? command.keyText : undefined,
|
||||
enabled: command ? command.enabled : false,
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_runCommand('${id}')`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildMenu() {
|
||||
const template = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Connect to database',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Open file',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_openFile()`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('save')`);
|
||||
},
|
||||
accelerator: 'Ctrl+S',
|
||||
id: 'save',
|
||||
},
|
||||
{
|
||||
label: 'Save As',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('saveAs')`);
|
||||
},
|
||||
accelerator: 'Ctrl+Shift+S',
|
||||
id: 'saveAs',
|
||||
},
|
||||
commandItem('new.connection'),
|
||||
commandItem('file.open'),
|
||||
commandItem('group.save'),
|
||||
commandItem('group.saveAs'),
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{
|
||||
label: 'New query',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Close all tabs',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
|
||||
},
|
||||
},
|
||||
{ role: 'minimize' },
|
||||
],
|
||||
submenu: [commandItem('new.query'), { type: 'separator' }, commandItem('tabs.closeAll'), { role: 'minimize' }],
|
||||
},
|
||||
|
||||
// {
|
||||
@@ -115,6 +91,7 @@ function buildMenu() {
|
||||
{ role: 'zoomout' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
commandItem('theme.changeTheme'),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -144,12 +121,7 @@ function buildMenu() {
|
||||
require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_showAbout()`);
|
||||
},
|
||||
},
|
||||
commandItem('about.show'),
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -157,10 +129,22 @@ function buildMenu() {
|
||||
return Menu.buildFromTemplate(template);
|
||||
}
|
||||
|
||||
ipcMain.on('update-menu', async (event, arg) => {
|
||||
const commands = await mainWindow.webContents.executeJavaScript(`dbgate_getCurrentTabCommands()`);
|
||||
mainMenu.getMenuItemById('save').enabled = !!commands.save;
|
||||
mainMenu.getMenuItemById('saveAs').enabled = !!commands.saveAs;
|
||||
ipcMain.on('update-commands', async (event, arg) => {
|
||||
commands = JSON.parse(arg);
|
||||
for (const key of Object.keys(commands)) {
|
||||
const menu = mainMenu.getMenuItemById(key);
|
||||
if (!menu) continue;
|
||||
const command = commands[key];
|
||||
|
||||
// rebuild menu
|
||||
if (menu.label != command.text || menu.accelerator != command.keyText) {
|
||||
mainMenu = buildMenu();
|
||||
mainWindow.setMenu(mainMenu);
|
||||
return;
|
||||
}
|
||||
|
||||
menu.enabled = command.enabled;
|
||||
}
|
||||
});
|
||||
|
||||
function createWindow() {
|
||||
@@ -186,7 +170,7 @@ function createWindow() {
|
||||
const startUrl =
|
||||
process.env.ELECTRON_START_URL ||
|
||||
url.format({
|
||||
pathname: path.join(__dirname, '../packages/web/build/index.html'),
|
||||
pathname: path.join(__dirname, '../packages/web/public/index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
});
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "3.9.6",
|
||||
"version": "4.0.0",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -9,7 +9,7 @@
|
||||
"start:api": "yarn workspace dbgate-api start",
|
||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||
"start:api:covid": "yarn workspace dbgate-api start:covid",
|
||||
"start:web": "yarn workspace dbgate-web start",
|
||||
"start:web": "yarn workspace dbgate-web dev",
|
||||
"start:sqltree": "yarn workspace dbgate-sqltree start",
|
||||
"start:tools": "yarn workspace dbgate-tools start",
|
||||
"start:datalib": "yarn workspace dbgate-datalib start",
|
||||
@@ -21,7 +21,7 @@
|
||||
"build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
||||
"build:app": "cd app && yarn install && yarn build",
|
||||
"build:api": "yarn workspace dbgate-api build",
|
||||
"build:web:docker": "yarn workspace dbgate-web build:docker",
|
||||
"build:web:docker": "yarn workspace dbgate-web build",
|
||||
"build:app:local": "cd app && yarn build:local",
|
||||
"start:app:local": "cd app && yarn start:local",
|
||||
"setCurrentVersion": "node setCurrentVersion",
|
||||
@@ -29,7 +29,7 @@
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --eletron",
|
||||
"prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/build/* docker -u 2 && copyfiles \"packages/web/build/**/*\" docker -u 2",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2",
|
||||
"prepare:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build",
|
||||
"prepare": "yarn build:lib",
|
||||
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-sqltree": "^3.9.5",
|
||||
"dbgate-tools": "^3.9.5",
|
||||
"dbgate-tools": "^4.0.0-rc.1",
|
||||
"eslint": "^6.8.0",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
@@ -49,7 +49,7 @@
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "nodemon src/index.js",
|
||||
"start": "node src/index.js",
|
||||
"start:portal": "env-cmd nodemon src/index.js",
|
||||
"start:covid": "env-cmd -f .covid-env nodemon src/index.js",
|
||||
"ts": "tsc",
|
||||
|
||||
@@ -106,10 +106,14 @@ module.exports = {
|
||||
|
||||
ping_meta: 'post',
|
||||
async ping({ conid, database }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
let existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
|
||||
if (existing) {
|
||||
existing.subprocess.send({ msgtype: 'ping' });
|
||||
} else {
|
||||
existing = await this.ensureOpened(conid, database);
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
connectionStatus: existing ? existing.status : null,
|
||||
@@ -153,6 +157,16 @@ module.exports = {
|
||||
// };
|
||||
},
|
||||
|
||||
sqlPreview_meta: 'post',
|
||||
async sqlPreview({ conid, database, objects, options }) {
|
||||
// wait for structure
|
||||
await this.structure({ conid, database });
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'sqlPreview', objects, options });
|
||||
return res;
|
||||
},
|
||||
|
||||
// runCommand_meta: 'post',
|
||||
// async runCommand({ conid, database, sql }) {
|
||||
// console.log(`Running SQL command , conid=${conid}, database=${database}, sql=${sql}`);
|
||||
|
||||
@@ -121,7 +121,13 @@ module.exports = {
|
||||
getStats_meta: 'get',
|
||||
getStats({ jslid }) {
|
||||
const file = `${getJslFileName(jslid)}.stats`;
|
||||
if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
if (fs.existsSync(file)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ const hasPermission = require('../utility/hasPermission');
|
||||
// }
|
||||
|
||||
const preinstallPluginMinimalVersions = {
|
||||
'dbgate-plugin-mssql': '1.1.0',
|
||||
'dbgate-plugin-mysql': '1.1.0',
|
||||
'dbgate-plugin-postgres': '1.1.0',
|
||||
'dbgate-plugin-mssql': '1.2.0',
|
||||
'dbgate-plugin-mysql': '1.2.1',
|
||||
'dbgate-plugin-postgres': '1.2.0',
|
||||
'dbgate-plugin-csv': '1.0.8',
|
||||
'dbgate-plugin-excel': '1.0.6',
|
||||
};
|
||||
@@ -149,7 +149,7 @@ module.exports = {
|
||||
return content.commands[command](args);
|
||||
},
|
||||
|
||||
authTypes_meta: 'post',
|
||||
authTypes_meta: 'get',
|
||||
async authTypes({ engine }) {
|
||||
const packageName = extractPackageName(engine);
|
||||
const content = requirePlugin(packageName);
|
||||
|
||||
@@ -3,6 +3,7 @@ const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const { SqlGenerator } = require('dbgate-tools');
|
||||
|
||||
let systemConnection;
|
||||
let storedConnection;
|
||||
@@ -93,6 +94,27 @@ async function handleQueryData({ msgid, sql }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSqlPreview({ msgid, objects, options }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
|
||||
try {
|
||||
const dmp = driver.createDumper();
|
||||
const generator = new SqlGenerator(analysedStructure, options, objects, dmp, driver, systemConnection);
|
||||
|
||||
await generator.dump();
|
||||
process.send({ msgtype: 'response', msgid, sql: dmp.s, isTruncated: generator.isTruncated });
|
||||
if (generator.isUnhandledException) {
|
||||
setTimeout(() => {
|
||||
console.log('Exiting because of unhandled exception');
|
||||
process.exit(0);
|
||||
}, 500);
|
||||
}
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, isError: true, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
// async function handleRunCommand({ msgid, sql }) {
|
||||
// await waitConnected();
|
||||
// const driver = engines(storedConnection);
|
||||
@@ -107,6 +129,7 @@ function handlePing() {
|
||||
const messageHandlers = {
|
||||
connect: handleConnect,
|
||||
queryData: handleQueryData,
|
||||
sqlPreview: handleSqlPreview,
|
||||
ping: handlePing,
|
||||
// runCommand: handleRunCommand,
|
||||
};
|
||||
|
||||
@@ -347,5 +347,6 @@ export function changeSetInsertNewRow(changeSet: ChangeSet, name?: NamedObjectIn
|
||||
}
|
||||
|
||||
export function changeSetContainsChanges(changeSet: ChangeSet) {
|
||||
if (!changeSet) return false;
|
||||
return changeSet.deletes.length > 0 || changeSet.updates.length > 0 || changeSet.inserts.length > 0;
|
||||
}
|
||||
|
||||
@@ -19,4 +19,6 @@ export interface MacroDefinition {
|
||||
export interface MacroSelectedCell {
|
||||
column: string;
|
||||
row: number;
|
||||
rowData: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import uuidv1 from 'uuid/v1';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import moment from 'moment';
|
||||
import { MacroDefinition, MacroSelectedCell } from './MacroDefinition';
|
||||
import { ChangeSet, setChangeSetValue } from './ChangeSet';
|
||||
import { GridDisplay } from './GridDisplay';
|
||||
|
||||
const getMacroFunction = {
|
||||
transformValue: code => `
|
||||
@@ -183,3 +185,55 @@ export function runMacro(
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function compileMacroFunction(macro: MacroDefinition, errors = []) {
|
||||
if (!macro) return null;
|
||||
let func;
|
||||
try {
|
||||
func = eval(getMacroFunction[macro.type](macro.code));
|
||||
return func;
|
||||
} catch (err) {
|
||||
errors.push(`Error compiling macro ${macro.name}: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function runMacroOnValue(compiledFunc, macroArgs, value, rowIndex, row, column, errors = []) {
|
||||
if (!compiledFunc) return value;
|
||||
try {
|
||||
const res = compiledFunc(value, macroArgs, modules, rowIndex, row, column);
|
||||
return res;
|
||||
} catch (err) {
|
||||
errors.push(`Error processing column ${column} on row ${rowIndex}: ${err.message}`);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function runMacroOnChangeSet(
|
||||
macro: MacroDefinition,
|
||||
macroArgs: {},
|
||||
selectedCells: MacroSelectedCell[],
|
||||
changeSet: ChangeSet,
|
||||
display: GridDisplay
|
||||
): ChangeSet {
|
||||
const errors = [];
|
||||
const compiledMacroFunc = compileMacroFunction(macro, errors);
|
||||
if (!compiledMacroFunc) return null;
|
||||
|
||||
let res = changeSet;
|
||||
for (const cell of selectedCells) {
|
||||
const definition = display.getChangeSetField(cell.rowData, cell.column, undefined);
|
||||
const macroResult = runMacroOnValue(
|
||||
compiledMacroFunc,
|
||||
macroArgs,
|
||||
cell.value,
|
||||
cell.row,
|
||||
cell.rowData,
|
||||
cell.column,
|
||||
errors
|
||||
);
|
||||
res = setChangeSetValue(res, definition, macroResult);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/parsimmon": "^1.10.1",
|
||||
"dbgate-tools": "^3.9.5",
|
||||
"dbgate-tools": "^4.0.0-rc.1",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"parsimmon": "^1.13.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "3.9.5",
|
||||
"version": "4.0.0-rc-2",
|
||||
"name": "dbgate-tools",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -35,4 +35,4 @@
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
EngineDriver,
|
||||
ForeignKeyInfo,
|
||||
FunctionInfo,
|
||||
NamedObjectInfo,
|
||||
PrimaryKeyInfo,
|
||||
ProcedureInfo,
|
||||
SqlDialect,
|
||||
TableInfo,
|
||||
TransformType,
|
||||
TriggerInfo,
|
||||
ViewInfo,
|
||||
IndexInfo,
|
||||
UniqueInfo,
|
||||
CheckInfo,
|
||||
} from 'dbgate-types';
|
||||
import _isString from 'lodash/isString';
|
||||
import _isNumber from 'lodash/isNumber';
|
||||
@@ -46,11 +55,12 @@ export class SqlDumper {
|
||||
}
|
||||
putValue(value) {
|
||||
if (value === null) this.putRaw('NULL');
|
||||
if (value === true) this.putRaw('1');
|
||||
if (value === false) this.putRaw('0');
|
||||
else if (value === true) this.putRaw('1');
|
||||
else if (value === false) this.putRaw('0');
|
||||
else if (_isString(value)) this.putStringValue(value);
|
||||
else if (_isNumber(value)) this.putRaw(value.toString());
|
||||
else if (_isDate(value)) this.putStringValue(new Date(value).toISOString());
|
||||
else this.putRaw('NULL');
|
||||
}
|
||||
putCmd(format, ...args) {
|
||||
this.put(format, ...args);
|
||||
@@ -260,4 +270,189 @@ export class SqlDumper {
|
||||
}
|
||||
|
||||
allowIdentityInsert(table: NamedObjectInfo, allow: boolean) {}
|
||||
enableConstraints(table: NamedObjectInfo, enabled: boolean) {}
|
||||
|
||||
comment(value: string) {
|
||||
if (!value) return;
|
||||
for (const line of value.split('\n')) {
|
||||
this.put(' -- %s', line.trimRight());
|
||||
}
|
||||
}
|
||||
|
||||
createView(obj: ViewInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
}
|
||||
dropView(obj: ViewInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^view %f', obj);
|
||||
}
|
||||
alterView(obj: ViewInfo) {
|
||||
this.putRaw(obj.createSql.replace(/create\s+view/i, 'ALTER VIEW'));
|
||||
this.endCommand();
|
||||
}
|
||||
changeViewSchema(obj: ViewInfo, newSchema: string) {}
|
||||
renameView(obj: ViewInfo, newSchema: string) {}
|
||||
|
||||
createProcedure(obj: ProcedureInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
}
|
||||
dropProcedure(obj: ProcedureInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^procedure %f', obj);
|
||||
}
|
||||
alterProcedure(obj: ProcedureInfo) {
|
||||
this.putRaw(obj.createSql.replace(/create\s+procedure/i, 'ALTER PROCEDURE'));
|
||||
this.endCommand();
|
||||
}
|
||||
changeProcedureSchema(obj: ProcedureInfo, newSchema: string) {}
|
||||
renameProcedure(obj: ProcedureInfo, newSchema: string) {}
|
||||
|
||||
createFunction(obj: FunctionInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
}
|
||||
dropFunction(obj: FunctionInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^function %f', obj);
|
||||
}
|
||||
alterFunction(obj: FunctionInfo) {
|
||||
this.putRaw(obj.createSql.replace(/create\s+function/i, 'ALTER FUNCTION'));
|
||||
this.endCommand();
|
||||
}
|
||||
changeFunctionSchema(obj: FunctionInfo, newSchema: string) {}
|
||||
renameFunction(obj: FunctionInfo, newSchema: string) {}
|
||||
|
||||
createTrigger(obj: TriggerInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
}
|
||||
dropTrigger(obj: TriggerInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^trigger %f', obj);
|
||||
}
|
||||
alterTrigger(obj: TriggerInfo) {
|
||||
this.putRaw(obj.createSql.replace(/create\s+trigger/i, 'ALTER TRIGGER'));
|
||||
this.endCommand();
|
||||
}
|
||||
changeTriggerSchema(obj: TriggerInfo, newSchema: string) {}
|
||||
renameTrigger(obj: TriggerInfo, newSchema: string) {}
|
||||
|
||||
dropConstraint(cnt: ConstraintInfo) {
|
||||
this.putCmd('^alter ^table %f ^drop ^constraint %i', cnt, cnt.constraintName);
|
||||
}
|
||||
dropForeignKey(fk: ForeignKeyInfo) {
|
||||
if (this.dialect.explicitDropConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^foreign ^key %i', fk, fk.constraintName);
|
||||
} else {
|
||||
this.dropConstraint(fk);
|
||||
}
|
||||
}
|
||||
createForeignKey(fk: ForeignKeyInfo) {
|
||||
this.put('^alter ^table %f ^add ', fk);
|
||||
this.createForeignKeyFore(fk);
|
||||
this.endCommand();
|
||||
}
|
||||
dropPrimaryKey(pk: PrimaryKeyInfo) {
|
||||
if (this.dialect.explicitDropConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^primary ^key', pk);
|
||||
} else {
|
||||
this.dropConstraint(pk);
|
||||
}
|
||||
}
|
||||
createPrimaryKey(pk: PrimaryKeyInfo) {
|
||||
this.putCmd(
|
||||
'^alter ^table %f ^add ^constraint %i ^primary ^key (%,i)',
|
||||
pk,
|
||||
pk.constraintName,
|
||||
pk.columns.map(x => x.columnName)
|
||||
);
|
||||
}
|
||||
|
||||
dropIndex(ix: IndexInfo) {}
|
||||
createIndex(ix: IndexInfo) {}
|
||||
|
||||
dropUnique(uq: UniqueInfo) {
|
||||
this.dropConstraint(uq);
|
||||
}
|
||||
createUniqueCore(uq: UniqueInfo) {
|
||||
this.put(
|
||||
'^constraint %i ^unique (%,i)',
|
||||
uq.constraintName,
|
||||
uq.columns.map(x => x.columnName)
|
||||
);
|
||||
}
|
||||
|
||||
createUnique(uq: UniqueInfo) {
|
||||
this.put('^alter ^table %f ^add ', uq);
|
||||
this.createUniqueCore(uq);
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
dropCheck(ch: CheckInfo) {
|
||||
this.dropConstraint(ch);
|
||||
}
|
||||
|
||||
createCheckCore(ch: CheckInfo) {
|
||||
this.put('^constraint %i ^check (%s)', ch.constraintName, ch.definition);
|
||||
}
|
||||
|
||||
createCheck(ch: CheckInfo) {
|
||||
this.put('^alter ^table %f ^add ', ch);
|
||||
this.createCheckCore(ch);
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string) {}
|
||||
|
||||
createColumn(table: TableInfo, column: ColumnInfo, constraints: ConstraintInfo[]) {
|
||||
this.put('^alter ^table %f ^add %i ', table, column.columnName);
|
||||
this.columnDefinition(column);
|
||||
this.inlineConstraints(constraints);
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
inlineConstraints(constrains: ConstraintInfo[]) {
|
||||
if (constrains == null) return;
|
||||
for (const cnt of constrains) {
|
||||
if (cnt.constraintType == 'primaryKey') {
|
||||
if (cnt.constraintName != null && !this.dialect.anonymousPrimaryKey) {
|
||||
this.put(' ^constraint %i', cnt.constraintName);
|
||||
}
|
||||
this.put(' ^primary ^key ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dropColumn(column: ColumnInfo) {
|
||||
this.putCmd('^alter ^table %f ^drop ^column %i', column, column.columnName);
|
||||
}
|
||||
|
||||
renameColumn(column: ColumnInfo, newName: string) {}
|
||||
|
||||
changeColumn(oldcol: ColumnInfo, newcol: ColumnInfo, constraints: ConstraintInfo[]) {}
|
||||
|
||||
dropTable(obj: TableInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^table %f', obj);
|
||||
}
|
||||
|
||||
changeTableSchema(obj: TableInfo, schema: string) {}
|
||||
|
||||
renameTable(obj: TableInfo, newname: string) {}
|
||||
|
||||
beginTransaction() {
|
||||
this.putCmd('^begin ^transaction');
|
||||
}
|
||||
|
||||
commitTransaction() {
|
||||
this.putCmd('^commit');
|
||||
}
|
||||
|
||||
alterProlog() {}
|
||||
alterEpilog() {}
|
||||
|
||||
selectTableIntoNewTable(sourceName: NamedObjectInfo, targetName: NamedObjectInfo) {
|
||||
this.putCmd('^select * ^into %f ^from %f', targetName, sourceName);
|
||||
}
|
||||
|
||||
truncateTable(name: NamedObjectInfo) {
|
||||
this.putCmd('^delete ^from %f', name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import {
|
||||
DatabaseInfo,
|
||||
EngineDriver,
|
||||
FunctionInfo,
|
||||
ProcedureInfo,
|
||||
TableInfo,
|
||||
TriggerInfo,
|
||||
ViewInfo,
|
||||
} from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import { SqlDumper } from './SqlDumper';
|
||||
import { extendDatabaseInfo } from './structureTools';
|
||||
|
||||
interface SqlGeneratorOptions {
|
||||
dropTables: boolean;
|
||||
checkIfTableExists: boolean;
|
||||
dropReferences: boolean;
|
||||
createTables: boolean;
|
||||
createReferences: boolean;
|
||||
createForeignKeys: boolean;
|
||||
createIndexes: boolean;
|
||||
insert: boolean;
|
||||
skipAutoincrementColumn: boolean;
|
||||
disableConstraints: boolean;
|
||||
omitNulls: boolean;
|
||||
truncate: boolean;
|
||||
|
||||
dropViews: boolean;
|
||||
checkIfViewExists: boolean;
|
||||
createViews: boolean;
|
||||
|
||||
dropProcedures: boolean;
|
||||
checkIfProcedureExists: boolean;
|
||||
createProcedures: boolean;
|
||||
|
||||
dropFunctions: boolean;
|
||||
checkIfFunctionExists: boolean;
|
||||
createFunctions: boolean;
|
||||
|
||||
dropTriggers: boolean;
|
||||
checkIfTriggerExists: boolean;
|
||||
createTriggers: boolean;
|
||||
}
|
||||
|
||||
interface SqlGeneratorObject {
|
||||
schemaName: string;
|
||||
pureName: string;
|
||||
objectTypeField: 'tables' | 'views' | 'procedures' | 'functions';
|
||||
}
|
||||
|
||||
export class SqlGenerator {
|
||||
private tables: TableInfo[];
|
||||
private views: ViewInfo[];
|
||||
private procedures: ProcedureInfo[];
|
||||
private functions: FunctionInfo[];
|
||||
private triggers: TriggerInfo[];
|
||||
public dbinfo: DatabaseInfo;
|
||||
public isTruncated = false;
|
||||
public isUnhandledException = false;
|
||||
|
||||
constructor(
|
||||
dbinfo: DatabaseInfo,
|
||||
public options: SqlGeneratorOptions,
|
||||
public objects: SqlGeneratorObject[],
|
||||
public dmp: SqlDumper,
|
||||
public driver: EngineDriver,
|
||||
public pool
|
||||
) {
|
||||
this.dbinfo = extendDatabaseInfo(dbinfo);
|
||||
this.tables = this.extract('tables');
|
||||
this.views = this.extract('views');
|
||||
this.procedures = this.extract('procedures');
|
||||
this.functions = this.extract('functions');
|
||||
this.triggers = this.extract('triggers');
|
||||
}
|
||||
|
||||
private handleException = error => {
|
||||
console.log('Unhandled error', error);
|
||||
this.isUnhandledException = true;
|
||||
};
|
||||
|
||||
async dump() {
|
||||
try {
|
||||
process.on('uncaughtException', this.handleException);
|
||||
|
||||
this.dropObjects(this.procedures, 'Procedure');
|
||||
if (this.checkDumper()) return;
|
||||
this.dropObjects(this.functions, 'Function');
|
||||
if (this.checkDumper()) return;
|
||||
this.dropObjects(this.views, 'View');
|
||||
if (this.checkDumper()) return;
|
||||
this.dropObjects(this.triggers, 'Trigger');
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.dropTables();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.createTables();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.truncateTables();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
await this.insertData();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.createForeignKeys();
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
this.createObjects(this.procedures, 'Procedure');
|
||||
if (this.checkDumper()) return;
|
||||
this.createObjects(this.functions, 'Function');
|
||||
if (this.checkDumper()) return;
|
||||
this.createObjects(this.views, 'View');
|
||||
if (this.checkDumper()) return;
|
||||
this.createObjects(this.triggers, 'Trigger');
|
||||
if (this.checkDumper()) return;
|
||||
} finally {
|
||||
process.off('uncaughtException', this.handleException);
|
||||
}
|
||||
}
|
||||
|
||||
createForeignKeys() {
|
||||
const fks = [];
|
||||
if (this.options.createForeignKeys) fks.push(..._.flatten(this.tables.map(x => x.foreignKeys || [])));
|
||||
if (this.options.createReferences) fks.push(..._.flatten(this.tables.map(x => x.dependencies || [])));
|
||||
for (const fk of _.uniqBy(fks, 'constraintName')) {
|
||||
this.dmp.createForeignKey(fk);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
|
||||
truncateTables() {
|
||||
if (this.options.truncate) {
|
||||
for (const table of this.tables) {
|
||||
this.dmp.truncateTable(table);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createTables() {
|
||||
if (this.options.createTables) {
|
||||
for (const table of this.tables) {
|
||||
this.dmp.createTable({
|
||||
...table,
|
||||
foreignKeys: [],
|
||||
dependencies: [],
|
||||
indexes: [],
|
||||
});
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
if (this.options.createIndexes) {
|
||||
for (const index of _.flatten(this.tables.map(x => x.indexes || []))) {
|
||||
this.dmp.createIndex(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async insertData() {
|
||||
if (!this.options.insert) return;
|
||||
|
||||
this.enableConstraints(false);
|
||||
|
||||
for (const table of this.tables) {
|
||||
await this.insertTableData(table);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
|
||||
this.enableConstraints(true);
|
||||
}
|
||||
|
||||
checkDumper() {
|
||||
if (this.dmp.s.length > 4000000) {
|
||||
if (!this.isTruncated) {
|
||||
this.dmp.putRaw('\n');
|
||||
this.dmp.comment(' *************** SQL is truncated ******************');
|
||||
this.dmp.putRaw('\n');
|
||||
}
|
||||
this.isTruncated = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
dropObjects(list, name) {
|
||||
if (this.options[`drop${name}s`]) {
|
||||
for (const item of list) {
|
||||
this.dmp[`drop${name}`](item, { testIfExists: this.options[`checkIf${name}Exists`] });
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createObjects(list, name) {
|
||||
if (this.options[`create${name}s`]) {
|
||||
for (const item of list) {
|
||||
this.dmp[`create${name}`](item);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dropTables() {
|
||||
if (this.options.dropReferences) {
|
||||
for (const fk of _.flatten(this.tables.map(x => x.dependencies || []))) {
|
||||
this.dmp.dropForeignKey(fk);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.dropTables) {
|
||||
for (const table of this.tables) {
|
||||
this.dmp.dropTable(table, { testIfExists: this.options.checkIfTableExists });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async insertTableData(table: TableInfo) {
|
||||
const dmpLocal = this.driver.createDumper();
|
||||
dmpLocal.put('^select * ^from %f', table);
|
||||
|
||||
const autoinc = table.columns.find(x => x.autoIncrement);
|
||||
if (autoinc && !this.options.skipAutoincrementColumn) {
|
||||
this.dmp.allowIdentityInsert(table, true);
|
||||
}
|
||||
|
||||
const readable = await this.driver.readQuery(this.pool, dmpLocal.s, table);
|
||||
await this.processReadable(table, readable);
|
||||
|
||||
if (autoinc && !this.options.skipAutoincrementColumn) {
|
||||
this.dmp.allowIdentityInsert(table, false);
|
||||
}
|
||||
}
|
||||
|
||||
processReadable(table: TableInfo, readable) {
|
||||
const columnsFiltered = this.options.skipAutoincrementColumn
|
||||
? table.columns.filter(x => !x.autoIncrement)
|
||||
: table.columns;
|
||||
const columnNames = columnsFiltered.map(x => x.columnName);
|
||||
let isClosed = false;
|
||||
let isHeaderRead = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
readable.on('data', chunk => {
|
||||
if (isClosed) return;
|
||||
if (!isHeaderRead) {
|
||||
isHeaderRead = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.checkDumper()) {
|
||||
isClosed = true;
|
||||
resolve(undefined);
|
||||
readable.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const columnNamesCopy = this.options.omitNulls ? columnNames.filter(col => chunk[col] != null) : columnNames;
|
||||
this.dmp.put(
|
||||
'^insert ^into %f (%,i) ^values (%,v);&n',
|
||||
table,
|
||||
columnNamesCopy,
|
||||
columnNamesCopy.map(col => chunk[col])
|
||||
);
|
||||
});
|
||||
readable.on('end', () => {
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extract(objectTypeField) {
|
||||
return this.dbinfo[objectTypeField].filter(x =>
|
||||
this.objects.find(
|
||||
y => x.pureName == y.pureName && x.schemaName == y.schemaName && y.objectTypeField == objectTypeField
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
enableConstraints(enabled) {
|
||||
if (this.options.disableConstraints) {
|
||||
if (this.driver.dialect.enableConstraintsPerTable) {
|
||||
for (const table of this.tables) {
|
||||
this.dmp.enableConstraints(table, enabled);
|
||||
}
|
||||
} else {
|
||||
this.dmp.enableConstraints(null, enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
|
||||
}
|
||||
dmp.putRaw(';');
|
||||
// require('fs').writeFileSync('/home/jena/test.sql', dmp.s);
|
||||
// console.log(dmp.s);
|
||||
await driver.query(pool, dmp.s);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,3 +8,5 @@ export * from './driverBase';
|
||||
export * from './SqlDumper';
|
||||
export * from './testPermission';
|
||||
export * from './splitPostgresQuery';
|
||||
export * from './SqlGenerator';
|
||||
export * from './structureTools';
|
||||
|
||||
@@ -38,12 +38,12 @@ export function findObjectLike(
|
||||
if (!dbinfo) return null;
|
||||
if (schemaName) {
|
||||
// @ts-ignore
|
||||
return dbinfo[objectTypeField].find(
|
||||
return dbinfo[objectTypeField]?.find(
|
||||
x => equalStringLike(x.pureName, pureName) && equalStringLike(x.schemaName, schemaName)
|
||||
);
|
||||
}
|
||||
// @ts-ignore
|
||||
return dbinfo[objectTypeField].find(x => equalStringLike(x.pureName, pureName));
|
||||
return dbinfo[objectTypeField]?.find(x => equalStringLike(x.pureName, pureName));
|
||||
}
|
||||
|
||||
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { DatabaseInfo } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||
const allForeignKeys = _.flatten(db.tables.map(x => x.foreignKeys));
|
||||
return {
|
||||
...db,
|
||||
tables: db.tables.map(table => ({
|
||||
...table,
|
||||
dependencies: allForeignKeys.filter(x => x.refSchemaName == table.schemaName && x.refTableName == table.pureName),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function fillTableExtendedInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
return {
|
||||
...db,
|
||||
tables: (db.tables || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'tables',
|
||||
columns: (obj.columns || []).map(column => ({
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
...column,
|
||||
})),
|
||||
primaryKey: obj.primaryKey
|
||||
? {
|
||||
...obj.primaryKey,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'primaryKey',
|
||||
}
|
||||
: undefined,
|
||||
foreignKeys: (obj.foreignKeys || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'foreignKey',
|
||||
})),
|
||||
indexes: (obj.indexes || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'index',
|
||||
})),
|
||||
checks: (obj.checks || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'check',
|
||||
})),
|
||||
uniques: (obj.uniques || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'unique',
|
||||
})),
|
||||
})),
|
||||
views: (db.views || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'views',
|
||||
})),
|
||||
procedures: (db.procedures || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'procedures',
|
||||
})),
|
||||
functions: (db.functions || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'functions',
|
||||
})),
|
||||
triggers: (db.triggers || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'triggers',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function extendDatabaseInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
return fillTableExtendedInfo(addTableDependencies(db));
|
||||
}
|
||||
Vendored
+16
-2
@@ -10,7 +10,7 @@ export interface ColumnReference {
|
||||
|
||||
export interface ConstraintInfo extends NamedObjectInfo {
|
||||
constraintName: string;
|
||||
constraintType: string;
|
||||
constraintType: 'primaryKey' | 'foreignKey' | 'index' | 'check' | 'unique';
|
||||
}
|
||||
|
||||
export interface ColumnsConstraintInfo extends ConstraintInfo {
|
||||
@@ -26,7 +26,18 @@ export interface ForeignKeyInfo extends ColumnsConstraintInfo {
|
||||
deleteAction: string;
|
||||
}
|
||||
|
||||
export interface ColumnInfo {
|
||||
export interface IndexInfo extends ColumnsConstraintInfo {
|
||||
isUnique: boolean;
|
||||
indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
|
||||
}
|
||||
|
||||
export interface UniqueInfo extends ColumnsConstraintInfo {}
|
||||
|
||||
export interface CheckInfo extends ConstraintInfo {
|
||||
definition: string;
|
||||
}
|
||||
|
||||
export interface ColumnInfo extends NamedObjectInfo {
|
||||
columnName: string;
|
||||
notNull: boolean;
|
||||
autoIncrement: boolean;
|
||||
@@ -58,6 +69,9 @@ export interface TableInfo extends DatabaseObjectInfo {
|
||||
primaryKey?: PrimaryKeyInfo;
|
||||
foreignKeys: ForeignKeyInfo[];
|
||||
dependencies?: ForeignKeyInfo[];
|
||||
indexes?: IndexInfo[];
|
||||
uniques?: UniqueInfo[];
|
||||
checks?: CheckInfo[];
|
||||
}
|
||||
|
||||
export interface ViewInfo extends SqlObjectInfo {
|
||||
|
||||
Vendored
+3
@@ -5,4 +5,7 @@ export interface SqlDialect {
|
||||
offsetFetchRangeSyntax?: boolean;
|
||||
quoteIdentifier(s: string): string;
|
||||
fallbackDataType?: string;
|
||||
explicitDropConstraint?: boolean;
|
||||
anonymousPrimaryKey?: boolean;
|
||||
enableConstraintsPerTable?: boolean;
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -27,6 +27,7 @@ export interface EngineAuthType {
|
||||
export interface EngineDriver {
|
||||
engine: string;
|
||||
title: string;
|
||||
defaultPort?: number;
|
||||
connect({ server, port, user, password, database }): any;
|
||||
query(pool: any, sql: string): Promise<QueryResult>;
|
||||
stream(pool: any, sql: string, options: StreamOptions);
|
||||
|
||||
Vendored
+7
@@ -21,6 +21,12 @@ export interface FileFormatDefinition {
|
||||
getOutputParams?: (sourceName, values) => any;
|
||||
}
|
||||
|
||||
export interface ThemeDefinition {
|
||||
className: string;
|
||||
themeName: string;
|
||||
themeType: 'light' | 'dark';
|
||||
}
|
||||
|
||||
export interface PluginDefinition {
|
||||
packageName: string;
|
||||
manifest: any;
|
||||
@@ -31,4 +37,5 @@ export interface ExtensionsDirectory {
|
||||
plugins: PluginDefinition[];
|
||||
fileFormats: FileFormatDefinition[];
|
||||
drivers: EngineDriver[];
|
||||
themes: ThemeDefinition[];
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/prop-types": "off",
|
||||
"no-unused-vars": "warn"
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
```
|
||||
+35
-52
@@ -1,69 +1,52 @@
|
||||
{
|
||||
"name": "dbgate-web",
|
||||
"version": "3.9.5",
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "cross-env BROWSER=none PORT=5000 react-scripts start",
|
||||
"build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
||||
"build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build",
|
||||
"build": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
||||
"prepublishOnly": "yarn build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"ts": "tsc"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"build": "cross-env API_URL=ORIGIN rollup -c",
|
||||
"dev": "cross-env API_URL=http://localhost:3000 rollup -c -w",
|
||||
"start": "sirv public",
|
||||
"validate": "svelte-check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/styled-components": "^4.4.2",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"typescript": "^3.7.4",
|
||||
"@ant-design/colors": "^5.0.0",
|
||||
"@mdi/font": "^5.8.55",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||
"@rollup/plugin-replace": "^2.4.1",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@tsconfig/svelte": "^1.0.0",
|
||||
"ace-builds": "^1.4.8",
|
||||
"axios": "^0.19.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"dbgate-datalib": "^3.9.5",
|
||||
"dbgate-sqltree": "^3.9.5",
|
||||
"dbgate-tools": "^3.9.5",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"dbgate-tools": "^4.0.0-rc.1",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"localforage": "^1.9.0",
|
||||
"markdown-to-jsx": "^7.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "^16.12.0",
|
||||
"react-ace": "^8.0.0",
|
||||
"react-chartjs-2": "^2.11.1",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-dropzone": "^11.2.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-json-view": "^1.19.1",
|
||||
"react-modal": "^3.11.1",
|
||||
"react-scripts": "3.3.0",
|
||||
"react-select": "^3.1.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"rollup": "^2.3.4",
|
||||
"rollup-plugin-copy": "^3.3.0",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.0.0",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"sql-formatter": "^2.3.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"svelte": "^3.35.0",
|
||||
"svelte-check": "^1.0.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.3",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^5.9.55",
|
||||
"file-selector": "^0.2.4",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sirv-cli": "^1.0.0",
|
||||
"svelte-json-tree": "^0.1.0",
|
||||
"svelte-markdown": "^0.1.4",
|
||||
"svelte-select": "^3.17.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+419
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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;
|
||||
}
|
||||
.flexcol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.flex1 {
|
||||
flex: 1;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
.color-icon-blue {
|
||||
color: var(--theme-icon-blue);
|
||||
}
|
||||
|
||||
.color-icon-green {
|
||||
color: var(--theme-icon-green);
|
||||
}
|
||||
|
||||
.color-icon-red {
|
||||
color: var(--theme-icon-red);
|
||||
}
|
||||
|
||||
.color-icon-gold {
|
||||
color: var(--theme-icon-gold);
|
||||
}
|
||||
|
||||
.color-icon-yellow {
|
||||
color: var(--theme-icon-yellow);
|
||||
}
|
||||
|
||||
.color-icon-magenta {
|
||||
color: var(--theme-icon-magenta);
|
||||
}
|
||||
|
||||
|
||||
.color-icon-inv-green {
|
||||
color: var(--theme-icon-inv-green);
|
||||
}
|
||||
|
||||
.color-icon-inv-red {
|
||||
color: var(--theme-icon-inv-red);
|
||||
}
|
||||
@@ -2,43 +2,28 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description"
|
||||
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>DbGate</title>
|
||||
<title>DbGate</title>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description"
|
||||
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
|
||||
|
||||
<link rel='icon' type='image/png' href='favicon.ico'>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<link rel='stylesheet' href='global.css'>
|
||||
<link rel='stylesheet' href='dimensions.css'>
|
||||
<link rel='stylesheet' href='bulma.css'>
|
||||
<link rel='stylesheet' href='icon-colors.css'>
|
||||
<link rel='stylesheet' href='build/bundle.css'>
|
||||
<link rel='stylesheet' href='build/fonts/materialdesignicons.css'>
|
||||
|
||||
<script defer src='build/bundle.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">Loading DbGate...</div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,2 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
@@ -1,16 +0,0 @@
|
||||
body {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
div {
|
||||
color: white;
|
||||
font-size: 25pt;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
margin-top: 40px;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="splash.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>Starting DbGate...</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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;
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import CommandListener from './commands/CommandListener.svelte';
|
||||
|
||||
import PluginsProvider from './plugins/PluginsProvider.svelte';
|
||||
import Screen from './Screen.svelte';
|
||||
import ErrorHandler from './utility/ErrorHandler.svelte';
|
||||
</script>
|
||||
|
||||
<ErrorHandler />
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<script>
|
||||
import WidgetContainer from './widgets/WidgetContainer.svelte';
|
||||
import WidgetIconPanel from './widgets/WidgetIconPanel.svelte';
|
||||
import {
|
||||
currentTheme,
|
||||
currentThemeDefinition,
|
||||
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';
|
||||
|
||||
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
|
||||
</script>
|
||||
|
||||
<div class={`${$currentTheme} ${currentThemeType} 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;
|
||||
background: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
.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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import contextMenu from '../utility/contextMenu';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import CheckboxField from '../forms/CheckboxField.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let icon;
|
||||
export let title;
|
||||
export let data;
|
||||
export let module;
|
||||
|
||||
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;
|
||||
export let checkedObjectsStore = null;
|
||||
export let disableContextMenu = false;
|
||||
|
||||
$: isChecked = checkedObjectsStore && $checkedObjectsStore.find(x => module.extractKey(data) == module.extractKey(x));
|
||||
|
||||
function handleExpand() {
|
||||
dispatch('expand');
|
||||
}
|
||||
function handleClick() {
|
||||
if (checkedObjectsStore) {
|
||||
if (isChecked) {
|
||||
checkedObjectsStore.update(x => x.filter(y => module.extractKey(data) != module.extractKey(y)));
|
||||
} else {
|
||||
checkedObjectsStore.update(x => [...x, data]);
|
||||
}
|
||||
} else {
|
||||
dispatch('click');
|
||||
}
|
||||
}
|
||||
|
||||
function setChecked(value) {
|
||||
if (!value && isChecked) {
|
||||
checkedObjectsStore.update(x => x.filter(y => module.extractKey(data) != module.extractKey(y)));
|
||||
}
|
||||
if (value && !isChecked) {
|
||||
checkedObjectsStore.update(x => [...x, data]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="main"
|
||||
class:isBold
|
||||
draggable={true}
|
||||
on:click={handleClick}
|
||||
use:contextMenu={disableContextMenu ? null : menu}
|
||||
on:dragstart={e => {
|
||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
||||
}}
|
||||
>
|
||||
{#if checkedObjectsStore}
|
||||
<CheckboxField
|
||||
checked={!!isChecked}
|
||||
on:change={e => {
|
||||
// @ts-ignore
|
||||
setChecked(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#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>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script>
|
||||
import Link from '../elements/Link.svelte';
|
||||
|
||||
import { plusExpandIcon } from '../icons/expandIcons';
|
||||
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
import AppObjectListItem from './AppObjectListItem.svelte';
|
||||
|
||||
export let group;
|
||||
export let groupFunc;
|
||||
export let items;
|
||||
export let module;
|
||||
export let checkedObjectsStore = null;
|
||||
export let disableContextMenu = false;
|
||||
|
||||
let isExpanded = true;
|
||||
|
||||
$: filtered = items.filter(x => x.isMatched);
|
||||
$: countText = filtered.length < items.length ? `${filtered.length}/${items.length}` : `${items.length}`;
|
||||
|
||||
function handleCheckAll(isChecked) {
|
||||
checkedObjectsStore.update(checkList => {
|
||||
const res = isChecked
|
||||
? [
|
||||
...checkList,
|
||||
...filtered
|
||||
.filter(x => !checkList.find(y => module.extractKey(x.data) == module.extractKey(y)))
|
||||
.map(x => x.data),
|
||||
]
|
||||
: checkList.filter(x => groupFunc(x) != group);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="group" on:click={() => (isExpanded = !isExpanded)}>
|
||||
<span class="expand-icon">
|
||||
<FontIcon icon={plusExpandIcon(isExpanded)} />
|
||||
</span>
|
||||
|
||||
{group}
|
||||
{items && `(${countText})`}
|
||||
</div>
|
||||
|
||||
{#if isExpanded}
|
||||
{#if checkedObjectsStore}
|
||||
<div class="ml-2">
|
||||
<Link onClick={() => handleCheckAll(true)}>Check all</Link>
|
||||
|
|
||||
<Link onClick={() => handleCheckAll(false)}>Uncheck all</Link>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each filtered as item (module.extractKey(item.data))}
|
||||
<AppObjectListItem
|
||||
{...$$restProps}
|
||||
{module}
|
||||
data={item.data}
|
||||
{checkedObjectsStore}
|
||||
on:objectClick
|
||||
{disableContextMenu}
|
||||
/>
|
||||
{/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);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<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 checkedObjectsStore = null;
|
||||
export let disableContextMenu = false;
|
||||
|
||||
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}
|
||||
{checkedObjectsStore}
|
||||
{groupFunc}
|
||||
{disableContextMenu}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each filtered as data (module.extractKey(data))}
|
||||
<AppObjectListItem
|
||||
{module}
|
||||
{subItemsComponent}
|
||||
{expandOnClick}
|
||||
{data}
|
||||
{isExpandable}
|
||||
on:objectClick
|
||||
{expandIconFunc}
|
||||
{checkedObjectsStore}
|
||||
{disableContextMenu}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -0,0 +1,61 @@
|
||||
<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;
|
||||
export let checkedObjectsStore = null;
|
||||
export let disableContextMenu = false;
|
||||
|
||||
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)}
|
||||
{checkedObjectsStore}
|
||||
{module}
|
||||
{disableContextMenu}
|
||||
/>
|
||||
|
||||
{#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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -0,0 +1,147 @@
|
||||
<script context="module">
|
||||
const getContextMenu = (data, $openedConnections) => () => {
|
||||
const config = getCurrentConfig();
|
||||
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 });
|
||||
};
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete connection ${data.displayName || data.server}?`,
|
||||
onConfirm: () => axiosInstance.post('connections/delete', data),
|
||||
});
|
||||
};
|
||||
const handleCreateDatabase = () => {
|
||||
showModal(InputTextModal, {
|
||||
header: 'Create database',
|
||||
value: 'newdb',
|
||||
label: 'Database name',
|
||||
onConfirm: name =>
|
||||
axiosInstance.post('server-connections/create-database', {
|
||||
conid: data._id,
|
||||
name,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
return [
|
||||
config.runAsPortal == false && [
|
||||
{
|
||||
text: 'Edit',
|
||||
onClick: handleEdit,
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
!$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,
|
||||
},
|
||||
$openedConnections.includes(data._id) && {
|
||||
text: 'Create database',
|
||||
onClick: handleCreateDatabase,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
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, getCurrentConfig, openedConnections } from '../stores';
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ConnectionModal from '../modals/ConnectionModal.svelte';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import InputTextModal from '../modals/InputTextModal.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;
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = props => props.name;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { getDefaultFileFormat } from '../plugins/fileformats';
|
||||
import { currentDatabase, extensions } from '../stores';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
export let data;
|
||||
|
||||
const handleNewQuery = () => {
|
||||
const { connection, name } = data;
|
||||
const tooltip = `${connection.displayName || connection.server}\n${name}`;
|
||||
openNewTab({
|
||||
title: 'Query #',
|
||||
icon: 'img sql-file',
|
||||
tooltip,
|
||||
tabComponent: 'QueryTab',
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
const { connection, name } = data;
|
||||
|
||||
showModal(ImportExportModal, {
|
||||
initialValues: {
|
||||
sourceStorageType: getDefaultFileFormat($extensions).storageType,
|
||||
targetStorageType: 'database',
|
||||
targetConnectionId: connection._id,
|
||||
targetDatabaseName: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const { connection, name } = data;
|
||||
|
||||
showModal(ImportExportModal, {
|
||||
initialValues: {
|
||||
targetStorageType: getDefaultFileFormat($extensions).storageType,
|
||||
sourceStorageType: 'database',
|
||||
sourceConnectionId: connection._id,
|
||||
sourceDatabaseName: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ onClick: handleNewQuery, text: 'New query' },
|
||||
{ onClick: handleImport, text: 'Import' },
|
||||
{ onClick: handleExport, text: 'Export' },
|
||||
];
|
||||
}
|
||||
</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)}
|
||||
menu={createMenu}
|
||||
/>
|
||||
@@ -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;
|
||||
@@ -0,0 +1,383 @@
|
||||
<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',
|
||||
},
|
||||
{
|
||||
label: 'SQL: SELECT',
|
||||
sqlTemplate: 'SELECT',
|
||||
},
|
||||
{
|
||||
label: 'SQL Generator: CREATE TABLE',
|
||||
sqlGeneratorProps: {
|
||||
createTables: true,
|
||||
createIndexes: true,
|
||||
createForeignKeys: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'SQL Generator: DROP TABLE',
|
||||
sqlGeneratorProps: {
|
||||
dropTables: true,
|
||||
dropReferences: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'SQL Generator: INSERT',
|
||||
sqlGeneratorProps: {
|
||||
insert: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
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',
|
||||
},
|
||||
{
|
||||
label: 'SQL: SELECT',
|
||||
sqlTemplate: 'SELECT',
|
||||
},
|
||||
{
|
||||
label: 'SQL Generator: CREATE VIEW',
|
||||
sqlGeneratorProps: {
|
||||
createViews: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'SQL Generator: DROP VIEW',
|
||||
sqlGeneratorProps: {
|
||||
dropViews: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
procedures: [
|
||||
{
|
||||
label: 'SQL: CREATE PROCEDURE',
|
||||
sqlTemplate: 'CREATE OBJECT',
|
||||
},
|
||||
{
|
||||
label: 'SQL: EXECUTE',
|
||||
sqlTemplate: 'EXECUTE PROCEDURE',
|
||||
},
|
||||
{
|
||||
label: 'SQL Generator: CREATE PROCEDURE',
|
||||
sqlGeneratorProps: {
|
||||
createProcedures: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'SQL Generator: DROP PROCEDURE',
|
||||
sqlGeneratorProps: {
|
||||
dropProcedures: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
label: 'SQL: CREATE FUNCTION',
|
||||
sqlTemplate: 'CREATE OBJECT',
|
||||
},
|
||||
{
|
||||
label: 'SQL Generator: CREATE FUNCTION',
|
||||
sqlGeneratorProps: {
|
||||
createFunctions: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'SQL Generator: DROP FUNCTION',
|
||||
sqlGeneratorProps: {
|
||||
dropFunctions: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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';
|
||||
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
|
||||
|
||||
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 if (menu.sqlGeneratorProps) {
|
||||
showModal(SqlGeneratorModal, {
|
||||
initialObjects: [data],
|
||||
initialConfig: menu.sqlGeneratorProps,
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
});
|
||||
} 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;
|
||||
@@ -0,0 +1,101 @@
|
||||
<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';
|
||||
import FavoriteModal from '../modals/FavoriteModal.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
const editFavorite = () => {
|
||||
showModal(FavoriteModal, { 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;
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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" />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import JSONTree from 'svelte-json-tree';
|
||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||
|
||||
export let selection;
|
||||
|
||||
let json = null;
|
||||
let error = null;
|
||||
|
||||
$: try {
|
||||
json = JSON.parse(selection[0].value);
|
||||
error = null;
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<ErrorInfo message="Error parsing JSON" />
|
||||
{:else}
|
||||
<div class="outer">
|
||||
<div class="inner">
|
||||
<JSONTree value={json} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.outer {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
.inner {
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
:global(.theme-type-dark) .inner {
|
||||
--json-tree-string-color: #efc5c5;
|
||||
--json-tree-symbol-color: #efc5c5;
|
||||
--json-tree-boolean-color: #a6b3f5;
|
||||
--json-tree-function-color: #a6b3f5;
|
||||
--json-tree-number-color: #bfbdf2;
|
||||
--json-tree-label-color: #e9aaed;
|
||||
--json-tree-arrow-color: #d4d4d4;
|
||||
--json-tree-null-color: #dcdcdc;
|
||||
--json-tree-undefined-color: #dcdcdc;
|
||||
--json-tree-date-color: #dcdcdc;
|
||||
}
|
||||
</style>
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
export let selection;
|
||||
export let wrap;
|
||||
</script>
|
||||
|
||||
<textarea class="flex1" {wrap} readonly value={selection.map(cell => cell.value).join('\n')} />
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import TextCellView from './TextCellView.svelte';
|
||||
export let selection;
|
||||
</script>
|
||||
|
||||
<TextCellView wrap="no" {selection} />
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import TextCellView from './TextCellView.svelte';
|
||||
export let selection;
|
||||
</script>
|
||||
|
||||
<TextCellView wrap="hard" {selection} />
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<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, themeDef) {
|
||||
if (!freeData || !labelColumn || !dataColumns || !freeData.rows || dataColumns.length == 0) return null;
|
||||
const palettes = themeDef?.themeType == 'dark' ? presetDarkPalettes : presetPalettes;
|
||||
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 = palettes[color][4] + '80';
|
||||
borderColor = 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];
|
||||
}
|
||||
</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';
|
||||
import { currentThemeDefinition } from '../stores';
|
||||
|
||||
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,
|
||||
$currentThemeDefinition
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="wrapper" bind:clientWidth bind:clientHeight>
|
||||
{#if chartData}
|
||||
{#key `${$values.chartType}|${clientWidth}|${clientHeight}`}
|
||||
<ChartCore
|
||||
width={clientWidth}
|
||||
height={clientHeight}
|
||||
data={chartData[0]}
|
||||
type={$values.chartType}
|
||||
options={chartData[1]}
|
||||
{menu}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,7 @@
|
||||
import { dumpSqlSelect, Select } from 'dbgate-sqltree';
|
||||
import { EngineDriver } from 'dbgate-types';
|
||||
import axios from '../utility/axios';
|
||||
import axiosInstance from '../utility/axiosInstance';
|
||||
import _ from 'lodash';
|
||||
import { extractDataColumns } from './DataChart';
|
||||
|
||||
export async function loadChartStructure(driver: EngineDriver, conid, database, sql) {
|
||||
const select: Select = {
|
||||
@@ -17,7 +16,7 @@ export async function loadChartStructure(driver: EngineDriver, conid, database,
|
||||
|
||||
const dmp = driver.createDumper();
|
||||
dumpSqlSelect(dmp, select);
|
||||
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
||||
const resp = await axiosInstance.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
||||
if (resp.data.errorMessage) throw new Error(resp.data.errorMessage);
|
||||
return resp.data.columns.map(x => x.columnName);
|
||||
}
|
||||
@@ -75,7 +74,7 @@ export async function loadChartData(driver: EngineDriver, conid, database, sql,
|
||||
|
||||
const dmp = driver.createDumper();
|
||||
dumpSqlSelect(dmp, select);
|
||||
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
||||
const resp = await axiosInstance.post('database-connections/query-data', { conid, database, sql: dmp.s });
|
||||
let { rows, columns } = resp.data;
|
||||
if (truncateFrom == 'end' && rows) {
|
||||
rows = _.reverse([...rows]);
|
||||
@@ -103,3 +102,21 @@ export async function loadChartData(driver: EngineDriver, conid, database, sql,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
export function extractDataColumns(values) {
|
||||
const dataColumns = [];
|
||||
for (const key in values) {
|
||||
if (key.startsWith('dataColumn_') && values[key]) {
|
||||
dataColumns.push(key.substring('dataColumn_'.length));
|
||||
}
|
||||
}
|
||||
return dataColumns;
|
||||
}
|
||||
export function extractDataColumnColors(values, dataColumns) {
|
||||
const res = {};
|
||||
for (const column of dataColumns) {
|
||||
const color = values[`dataColumnColor_${column}`];
|
||||
if (color) res[column] = color;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts" context="module">
|
||||
import { commands, visibleCommandPalette } 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();
|
||||
else if (command.getSubCommands) visibleCommandPalette.set(command);
|
||||
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} />
|
||||
@@ -0,0 +1,138 @@
|
||||
<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 { 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 filter = '';
|
||||
const domItems = {};
|
||||
|
||||
$: selectedIndex = true ? 0 : filter;
|
||||
$: parentCommand = _.isPlainObject($visibleCommandPalette) ? $visibleCommandPalette : null;
|
||||
|
||||
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) {
|
||||
$visibleCommandPalette = 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) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCommand(filteredItems[selectedIndex]);
|
||||
}
|
||||
if (e.keyCode == keycodes.escape) $visibleCommandPalette = false;
|
||||
|
||||
if (e.keyCode == keycodes.pageDown) selectedIndex = Math.min(selectedIndex + 15, filteredItems.length - 1);
|
||||
if (e.keyCode == keycodes.pageUp) selectedIndex = Math.max(selectedIndex - 15, 0);
|
||||
}
|
||||
|
||||
$: if (domItems[selectedIndex]) domItems[selectedIndex].scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
</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}
|
||||
placeholder={parentCommand?.text || ''}
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
{#each filteredItems as command, index}
|
||||
<div
|
||||
class="command"
|
||||
class:selected={index == selectedIndex}
|
||||
on:click={() => handleCommand(command)}
|
||||
bind:this={domItems[index]}
|
||||
>
|
||||
<div>{command.text}</div>
|
||||
{#if command.keyText}
|
||||
<div class="shortcut">{command.keyText}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
width: 500px;
|
||||
background: var(--theme-bg-2);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import _ from 'lodash';
|
||||
import { recentDatabases, currentDatabase, getRecentDatabases } from '../stores';
|
||||
import registerCommand from './registerCommand';
|
||||
|
||||
currentDatabase.subscribe(value => {
|
||||
if (!value) return;
|
||||
recentDatabases.update(list => {
|
||||
const res = [
|
||||
value,
|
||||
..._.compact(list).filter(x => x.name != value.name || x.connection?._id != value.connection?._id),
|
||||
].slice(0, 10);
|
||||
return res;
|
||||
});
|
||||
});
|
||||
|
||||
function switchDatabaseCommand(db) {
|
||||
return {
|
||||
text: `${db.name} on ${db?.connection?.displayName || db?.connection?.server}`,
|
||||
onClick: () => currentDatabase.set(db),
|
||||
};
|
||||
}
|
||||
|
||||
registerCommand({
|
||||
id: 'database.switch',
|
||||
category: 'Database',
|
||||
name: 'Change to recent',
|
||||
keyText: 'Ctrl+D',
|
||||
getSubCommands: () => getRecentDatabases().map(switchDatabaseCommand),
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getCommands, visibleCommandPalette } 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();
|
||||
} else if (command.getSubCommands) {
|
||||
visibleCommandPalette.set(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
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 SqlGeneratorModal from '../modals/SqlGeneratorModal.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';
|
||||
import { getCurrentConfig, getCurrentDatabase } from '../stores';
|
||||
import './recentDatabaseSwitch';
|
||||
|
||||
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',
|
||||
toolbarName: 'Change theme',
|
||||
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 new-connection',
|
||||
toolbarName: 'Add connection',
|
||||
category: 'New',
|
||||
toolbarOrder: 1,
|
||||
name: 'Connection',
|
||||
testEnabled: () => !getCurrentConfig()?.runAsPortal,
|
||||
onClick: () => showModal(ConnectionModal),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.query',
|
||||
category: 'New',
|
||||
icon: 'icon file',
|
||||
toolbar: true,
|
||||
toolbarOrder: 2,
|
||||
name: 'Query',
|
||||
toolbarName: 'New 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 },
|
||||
}),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'sql.generator',
|
||||
category: 'SQL',
|
||||
name: 'SQL Generator',
|
||||
toolbar: true,
|
||||
icon: 'icon sql-generator',
|
||||
testEnabled: () => getCurrentDatabase() != null,
|
||||
onClick: () =>
|
||||
showModal(SqlGeneratorModal, {
|
||||
conid: getCurrentDatabase()?.connection?._id,
|
||||
database: getCurrentDatabase()?.name,
|
||||
}),
|
||||
});
|
||||
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,12 @@ import {
|
||||
findExistingChangeSetItem,
|
||||
getChangeSetInsertedRows,
|
||||
GridDisplay,
|
||||
MacroDefinition,
|
||||
MacroSelectedCell,
|
||||
revertChangeSetRowChanges,
|
||||
setChangeSetValue,
|
||||
compileMacroFunction,
|
||||
runMacroOnValue,
|
||||
} from 'dbgate-datalib';
|
||||
import Grider, { GriderRowStatus } from './Grider';
|
||||
|
||||
@@ -21,8 +25,18 @@ export default class ChangeSetGrider extends Grider {
|
||||
private rowStatusCache;
|
||||
private rowDefinitionsCache;
|
||||
private batchChangeSet: ChangeSet;
|
||||
private _errors = null;
|
||||
private compiledMacroFunc;
|
||||
|
||||
constructor(public sourceRows: any[], public changeSetState, public dispatchChangeSet, public display: GridDisplay) {
|
||||
constructor(
|
||||
public sourceRows: any[],
|
||||
public changeSetState,
|
||||
public dispatchChangeSet,
|
||||
public display: GridDisplay,
|
||||
public macro: MacroDefinition,
|
||||
public macroArgs: {},
|
||||
public selectedCells: MacroSelectedCell[]
|
||||
) {
|
||||
super();
|
||||
this.changeSet = changeSetState && changeSetState.value;
|
||||
this.insertedRows = getChangeSetInsertedRows(this.changeSet, display.baseTable);
|
||||
@@ -32,6 +46,11 @@ export default class ChangeSetGrider extends Grider {
|
||||
this.rowStatusCache = {};
|
||||
this.rowDefinitionsCache = {};
|
||||
this.batchChangeSet = null;
|
||||
this.compiledMacroFunc = compileMacroFunction(macro, this._errors);
|
||||
}
|
||||
|
||||
get errors() {
|
||||
return this._errors;
|
||||
}
|
||||
|
||||
getRowSource(index: number) {
|
||||
@@ -49,7 +68,11 @@ export default class ChangeSetGrider extends Grider {
|
||||
const insertedRowIndex = this.getInsertedRowIndex(index);
|
||||
const rowDefinition = this.display.getChangeSetRow(row, insertedRowIndex);
|
||||
const [matchedField, matchedChangeSetItem] = findExistingChangeSetItem(this.changeSet, rowDefinition);
|
||||
const rowUpdated = matchedChangeSetItem ? { ...row, ...matchedChangeSetItem.fields } : row;
|
||||
const rowUpdated = matchedChangeSetItem
|
||||
? { ...row, ...matchedChangeSetItem.fields }
|
||||
: this.compiledMacroFunc
|
||||
? { ...row }
|
||||
: row;
|
||||
let status = 'regular';
|
||||
if (matchedChangeSetItem && matchedField == 'updates') status = 'updated';
|
||||
if (matchedField == 'deletes') status = 'deleted';
|
||||
@@ -59,6 +82,23 @@ export default class ChangeSetGrider extends Grider {
|
||||
modifiedFields:
|
||||
matchedChangeSetItem && matchedChangeSetItem.fields ? new Set(Object.keys(matchedChangeSetItem.fields)) : null,
|
||||
};
|
||||
|
||||
if (this.compiledMacroFunc) {
|
||||
for (const cell of this.selectedCells) {
|
||||
if (cell.row != index) continue;
|
||||
const newValue = runMacroOnValue(
|
||||
this.compiledMacroFunc,
|
||||
this.macroArgs,
|
||||
rowUpdated[cell.column],
|
||||
index,
|
||||
rowUpdated,
|
||||
cell.column,
|
||||
this._errors
|
||||
);
|
||||
rowUpdated[cell.column] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
this.rowDataCache[index] = rowUpdated;
|
||||
this.rowStatusCache[index] = rowStatus;
|
||||
this.rowDefinitionsCache[index] = rowDefinition;
|
||||
@@ -143,10 +183,10 @@ export default class ChangeSetGrider extends Grider {
|
||||
this.dispatchChangeSet({ type: 'redo' });
|
||||
}
|
||||
get canUndo() {
|
||||
return this.changeSetState.canUndo;
|
||||
return this.changeSetState?.canUndo;
|
||||
}
|
||||
get canRedo() {
|
||||
return this.changeSetState.canRedo;
|
||||
return this.changeSetState?.canRedo;
|
||||
}
|
||||
get containsChanges() {
|
||||
return changeSetContainsChanges(this.changeSet);
|
||||
@@ -155,10 +195,10 @@ export default class ChangeSetGrider extends Grider {
|
||||
return this.insertedRows.length > 0;
|
||||
}
|
||||
|
||||
static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
||||
return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
||||
}
|
||||
static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
||||
return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
|
||||
}
|
||||
// static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
||||
// return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
||||
// }
|
||||
// static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
||||
// return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
|
||||
// }
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user