Compare commits

..

107 Commits

Author SHA1 Message Date
Jan Prochazka 8f5d866905 v3.8.13 2020-12-14 20:54:30 +01:00
Jan Prochazka ed15db4c20 fix 2020-12-14 20:52:49 +01:00
Jan Prochazka a7926a1a71 save favorite content 2020-12-14 20:47:22 +01:00
Jan Prochazka 3a89d1a07b share tab content 2020-12-14 20:24:21 +01:00
Jan Prochazka 808267d97a share tab content 2020-12-14 18:51:51 +01:00
Jan Prochazka 4ab59e09d7 link to open favorite 2020-12-13 09:27:55 +01:00
Jan Prochazka c108c7d787 v3.8.12 2020-12-12 21:48:56 +01:00
Jan Prochazka fa5e128d45 fix 2020-12-12 21:48:42 +01:00
Jan Prochazka 1a181ff714 v3.8.11 2020-12-12 21:37:48 +01:00
Jan Prochazka 0f16f842bf favorite editor 2020-12-12 21:37:05 +01:00
Jan Prochazka e502f5e72c v3.8.10 2020-12-12 20:26:40 +01:00
Jan Prochazka 2978063ac8 removed markdown manifest, used favorites instead 2020-12-12 20:19:39 +01:00
Jan Prochazka ae0606cc84 refactor 2020-12-12 20:06:18 +01:00
Jan Prochazka 0c0c0356a6 favorite dialog 2020-12-12 20:02:55 +01:00
Jan Prochazka 1e447a8937 favorites refactor 2020-12-12 18:43:50 +01:00
Jan Prochazka 3dcc761c14 add to favorites basic functionality 2020-12-12 18:33:42 +01:00
Jan Prochazka b1a2093e6b open sql link from markdown 2020-12-12 10:42:26 +01:00
Jan Prochazka ed98a9e2da chart - undo, redo 2020-12-12 10:32:24 +01:00
Jan Prochazka 737298c6f3 chart - show relative values 2020-12-12 10:21:49 +01:00
Jan Prochazka 0081deb844 open existing tab, when possible 2020-12-12 09:34:55 +01:00
Jan Prochazka 0857757ce2 openNewTab refactor 2020-12-12 09:07:14 +01:00
Jan Prochazka f7bab744e6 v3.8.9 2020-12-10 18:48:19 +01:00
Jan Prochazka 0316cb16eb fix 2020-12-10 18:48:00 +01:00
Jan Prochazka adcab4ae69 fix 2020-12-10 18:45:57 +01:00
Jan Prochazka 728353d60a v3.8.8 2020-12-10 18:34:38 +01:00
Jan Prochazka ac4aa94976 markdown manifest 2020-12-10 18:34:02 +01:00
Jan Prochazka d502dc0dfd v3.8.7 2020-12-10 15:37:28 +01:00
Jan Prochazka 55d0c77536 markdown viewer 2020-12-10 15:36:45 +01:00
Jan Prochazka 79ddbd439f custom markdown pages - basic concept 2020-12-10 15:12:19 +01:00
Jan Prochazka 7a0883ea03 active chart menu, using markdown to jsx 2020-12-10 14:24:41 +01:00
Jan Prochazka 5256deb567 about modal, current version 2020-12-10 13:31:37 +01:00
Jan Prochazka f993e82b0b permissins (per instance) 2020-12-10 11:54:28 +01:00
Jan Prochazka 698756b9d2 single database configuration 2020-12-10 11:11:03 +01:00
Jan Prochazka 3921913742 rename saved file 2020-12-10 09:06:40 +01:00
Jan Prochazka 576bdd64a0 cleanup 2020-12-10 08:54:48 +01:00
Jan Prochazka 27a78facf5 chart date format 2020-12-10 08:45:14 +01:00
Jan Prochazka ee50604112 v3.8.6 2020-12-08 20:57:39 +01:00
Jan Prochazka 1c30eb337b fix 2020-12-08 20:57:20 +01:00
Jan Prochazka 6a78272cc5 v3.8.5 2020-12-08 20:00:54 +01:00
Jan Prochazka e05f01dce8 save chart files 2020-12-08 19:59:51 +01:00
Jan Prochazka a29026321f init added to shell script 2020-12-08 19:50:17 +01:00
Jan Prochazka f8ee3b92cf execute query from shell 2020-12-08 18:51:00 +01:00
Jan Prochazka e0c91214fd import-export advanced config 2020-12-08 17:34:40 +01:00
Jan Prochazka cc11e63cd7 error boundary 2020-12-08 17:24:47 +01:00
Jan Prochazka b47c9f81d2 transparent charts 2020-12-07 22:13:05 +01:00
Jan Prochazka fcb93015cc chart colors 2020-12-07 21:46:44 +01:00
Jan Prochazka fc6355126f save using ctrl+s 2020-12-07 19:49:37 +01:00
Jan Prochazka fd2747d166 charts 2020-12-06 11:01:58 +01:00
Jan Prochazka 3bb22ddc36 form style refactor + charts 2020-12-06 10:23:57 +01:00
Jan Prochazka 0c4d5b5356 active chart - load data from query 2020-12-05 20:47:31 +01:00
Jan Prochazka 61217a944b Merge commit '4eae30c0222ca46cc1dd3cb231f88f0d64badd04' 2020-12-05 18:33:51 +01:00
Jan Prochazka 5434302aa5 charts 2020-12-05 18:33:19 +01:00
Jan Prochazka 23d9c9279c save chart to editor data 2020-12-05 17:23:37 +01:00
dependabot[bot] 4eae30c022 Bump lodash from 4.17.15 to 4.17.20 in /app
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.20.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.20)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-05 15:58:18 +00:00
Jan Prochazka ac7816fc4b fix 2020-12-05 16:57:25 +01:00
Jan Prochazka 10dc7343ae submit action - run with enter 2020-12-05 16:52:57 +01:00
Jan Prochazka a3837083da removed formik, used own FormProvider instead 2020-12-05 16:16:04 +01:00
Jan Prochazka 1644587072 charts version 0 2020-12-05 15:13:08 +01:00
Jan Prochazka 6471141926 fix 2020-12-05 08:48:40 +01:00
Jan Prochazka 65197cf038 fix 2020-12-05 08:37:23 +01:00
Jan Prochazka 785c7e54ab better display engine 2020-12-05 08:35:29 +01:00
Jan Prochazka 9a2a945762 v3.8.4 2020-12-03 18:44:42 +01:00
Jan Prochazka 56eecb0836 shell script scheduler 2020-12-03 18:43:02 +01:00
Jan Prochazka 8d9cb51baa shell - save to server 2020-12-03 18:13:51 +01:00
Jan Prochazka f55049f212 shelltab uses require editor 2020-12-03 18:08:38 +01:00
Jan Prochazka dc6093e4fc free table editor uses useEditorData 2020-12-03 17:42:36 +01:00
Jan Prochazka 5df650bc51 use editor data optimalizactions 2020-12-03 17:24:37 +01:00
Jan Prochazka d23a013579 use editor data - sync fallback when called from beforeunload 2020-12-03 15:22:53 +01:00
Jan Prochazka fc5c0eb239 use editor data hook 2020-12-03 15:16:22 +01:00
Jan Prochazka 9a42d1d6bd added local forage 2020-12-03 13:22:54 +01:00
Jan Prochazka 942115acef save file app object 2020-12-03 12:58:04 +01:00
Jan Prochazka 22b2a62209 save file refactor 2020-12-03 12:26:40 +01:00
Jan Prochazka 0a3a1c9468 code cleanup 2020-12-03 12:03:37 +01:00
Jan Prochazka 655429693a app object refactor finished 2020-12-03 11:59:49 +01:00
Jan Prochazka 2afd46dc91 macro app object refactor 2020-12-03 11:44:18 +01:00
Jan Prochazka 9bf755ff25 app object refactor WIP 2020-12-03 11:35:27 +01:00
Jan Prochazka d693cb734b popup menu cleanup 2020-12-03 09:38:06 +01:00
Jan Prochazka 836f48c810 popup menu - fix popup placement 2020-12-03 09:36:29 +01:00
Jan Prochazka 327f2140cf showMenu refactor - context is now available inside menu 2020-12-03 09:33:29 +01:00
Jan Prochazka e4b605162e modal layer fix 2020-12-03 08:50:57 +01:00
Jan Prochazka e952d5c6f8 saved sql files - save to server (minimal version) 2020-12-02 20:02:58 +01:00
Jan Prochazka 203e490321 comment 2020-12-02 18:11:40 +01:00
Jan Prochazka 7264e52e6c icon 2020-12-01 21:19:08 +01:00
Jan Prochazka 40bca24dfb v3.8.3 2020-11-30 21:28:17 +01:00
Jan Prochazka 56c6ddd392 readme 2020-11-30 21:18:56 +01:00
Jan Prochazka 2cd370d519 packages-api v1.0.7 2020-11-30 20:54:20 +01:00
Jan Prochazka 42545a027f fixed deps 2020-11-30 20:53:43 +01:00
Jan Prochazka d1ed62ded9 packages-api v1.0.6 2020-11-30 20:18:25 +01:00
Jan Prochazka 2c9d3abe8c api package docs 2020-11-30 20:17:52 +01:00
Jan Prochazka a03a3416c2 packages-sqlitree v1.0.4 2020-11-30 20:14:07 +01:00
Jan Prochazka 45138b2bc1 sqltree readme 2020-11-30 20:13:31 +01:00
Jan Prochazka 3e57aab717 packages-types v1.0.2 2020-11-30 20:03:04 +01:00
Jan Prochazka 9aced8f01f fix + docs 2020-11-30 20:02:30 +01:00
Jan Prochazka 3e1e6f7164 import download fixes 2020-11-29 21:43:31 +01:00
Jan Prochazka 5aff88630a v3.8.2 2020-11-29 21:21:22 +01:00
Jan Prochazka 7e3555d84a download refactor 2020-11-29 21:20:27 +01:00
Jan Prochazka 6d7e7f97c7 data import download fixes 2020-11-29 20:12:11 +01:00
Jan Prochazka 0785c375a5 import - import files from URL 2020-11-29 19:47:56 +01:00
Jan Prochazka 0d68eeac63 upload file button 2020-11-29 10:49:01 +01:00
Jan Prochazka 634e63a3dc v3.8.1 2020-11-28 09:30:57 +01:00
Jan Prochazka f60fd9cc63 better connection dialog 2020-11-28 09:29:13 +01:00
Jan Prochazka 49628ad3cf server connection error report 2020-11-28 09:14:33 +01:00
Jan Prochazka 13a18eb556 version 2020-11-28 08:37:40 +01:00
Jan Prochazka 76ec548b4f packages-tools v1.0.5 2020-11-28 08:34:42 +01:00
Jan Prochazka 9c9c82a547 fixes 2020-11-28 08:27:40 +01:00
Jan Prochazka b1ccd16870 Merge branch 'plugins' 2020-11-27 21:35:52 +01:00
Jan Prochazka fedf2db847 v3.8.0 2020-11-27 21:34:19 +01:00
162 changed files with 3998 additions and 1678 deletions
+3
View File
@@ -33,6 +33,9 @@ jobs:
- name: yarn install
run: |
yarn install
- name: setCurrentVersion
run: |
yarn setCurrentVersion
- name: Publish
run: |
yarn run build:app
+3
View File
@@ -37,6 +37,9 @@ jobs:
- name: yarn install
run: |
yarn install
- name: setCurrentVersion
run: |
yarn setCurrentVersion
- name: Prepare docker image
run: |
yarn run prepare:docker
+13 -2
View File
@@ -21,6 +21,7 @@ DbGate is fast and efficient database administration tool. It is focused to work
* Archives - backup your data in JSON files on local filesystem (or on DbGate server, when using web application)
* Light and dark theme
* For detailed info, how to run DbGate in docker container, visit [docker hub](https://hub.docker.com/r/dbgate/dbgate)
* Extensible plugin architecture
![Screenshot](https://raw.githubusercontent.com/dbshell/dbgate/master/screenshot.png)
@@ -34,14 +35,23 @@ DbGate is fast and efficient database administration tool. It is focused to work
* There is plan to incorporate SQLite to support work with local datasets
* Platform independed - will run as web application in single docker container on server, or as application using Electron platform on Linux, Windows and Mac
## Plugins
Plugins are standard NPM packages published on [npmjs.com](https://www.npmjs.com).
See all [existing DbGate plugins](https://www.npmjs.com/search?q=keywords:dbgateplugin).
Visit [dbgate generator homepage](https://github.com/dbshell/generator-dbgate) to see, how to create your own plugin.
Currently following extensions can be implemented using plugins:
- File format parsers/writers
- Database engine connectors
## How Can I Contribute?
You're welcome to contribute to this project! Below are some ideas, how to contribute:
* Create plugins for new import/export formats
* Bug fixing
* Test Mac edition
* Improve linux package build, add to APT repository
* Auto-upgrade of electron application
* Support for new import/export formats
Any help is appreciated!
@@ -99,9 +109,10 @@ Some dbgate packages can be used also without DbGate. You can find them on [NPM
* [api](https://github.com/dbshell/dbgate/tree/master/packages/api) - backend, Javascript, ExpressJS [![NPM version](https://img.shields.io/npm/v/dbgate-api.svg)](https://www.npmjs.com/package/dbgate-api)
* [datalib](https://github.com/dbshell/dbgate/tree/master/packages/datalib) - TypeScript library for utility classes
* [app](https://github.com/dbshell/dbgate/tree/master/app) - application (JavaScript)
* [engines](https://github.com/dbshell/dbgate/tree/master/packages/engines) - drivers for database engine (mssql, mysql, postgres), analysing database structure, creating specific queries (JavaScript) [![NPM version](https://img.shields.io/npm/v/dbgate-engines.svg)](https://www.npmjs.com/package/dbgate-engines)
structure, creating specific queries (JavaScript) [![NPM version](https://img.shields.io/npm/v/dbgate-engines.svg)](https://www.npmjs.com/package/dbgate-engines)
* [filterparser](https://github.com/dbshell/dbgate/tree/master/packages/filterparser) - TypeScript library for parsing data filter expressions using parsimmon
* [sqltree](https://github.com/dbshell/dbgate/tree/master/packages/sqltree) - JSON representation of SQL query, functions converting to SQL (TypeScript) [![NPM version](https://img.shields.io/npm/v/dbgate-sqltree.svg)](https://www.npmjs.com/package/dbgate-sqltree)
* [types](https://github.com/dbshell/dbgate/tree/master/packages/types) - common TypeScript definitions [![NPM version](https://img.shields.io/npm/v/dbgate-types.svg)](https://www.npmjs.com/package/dbgate-types)
* [web](https://github.com/dbshell/dbgate/tree/master/packages/web) - frontend in React (JavaScript)
* [tools](https://github.com/dbshell/dbgate/tree/master/packages/tools) - various tools [![NPM version](https://img.shields.io/npm/v/dbgate-tools.svg)](https://www.npmjs.com/package/dbgate-tools)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dbgate",
"version": "3.7.33",
"version": "3.8.13",
"private": true,
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
"dependencies": {
+6
View File
@@ -99,6 +99,12 @@ function buildMenu() {
require('electron').shell.openExternal('https://hub.docker.com/r/dbgate/dbgate');
},
},
{
label: 'About',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_showAbout()`);
},
},
],
},
];
+3 -3
View File
@@ -1040,9 +1040,9 @@ lodash.isequal@^4.5.0:
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash@^4.17.10:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1"
+2
View File
@@ -7,6 +7,7 @@
"scripts": {
"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:sqltree": "yarn workspace dbgate-sqltree start",
"start:tools": "yarn workspace dbgate-tools start",
@@ -22,6 +23,7 @@
"build:web:docker": "yarn workspace dbgate-web build:docker",
"build:app:local": "cd app && yarn build:local",
"start:app:local": "cd app && yarn start:local",
"setCurrentVersion": "node setCurrentVersion",
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/build/* docker -u 2 && copyfiles \"packages/web/build/**/*\" docker -u 2",
"prepare:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build",
+12
View File
@@ -0,0 +1,12 @@
CONNECTIONS=mysql
LABEL_mysql=MySql
SERVER_mysql=dbgate.org
USER_mysql=reader
PASSWORD_mysql=CovidReader2020
PORT_mysql=3326
ENGINE_mysql=mysql@dbgate-plugin-mysql
SINGLE_CONNECTION=mysql
SINGLE_DATABASE=covid
PERMISSIONS=files/charts/read
+2
View File
@@ -0,0 +1,2 @@
version-tag-prefix packages-api-v
version-git-message "packages-api v%s"
+8 -40
View File
@@ -11,15 +11,19 @@ Allows run DbGate data-manipulation scripts.
This example exports table Customer info CSV file.
```javascript
const dbgateApi = require('dbgate-api');
const dbgatePluginMssql = require("dbgate-plugin-mssql");
const dbgatePluginCsv = require("dbgate-plugin-csv");
dbgateApi.registerPlugins(dbgatePluginMssql);
async function run() {
const reader = await dbgateApi.tableReader({
connection: { server: 'localhost', engine: 'mssql', user: 'sa', password: 'xxxx', database: 'Chinook' },
schemaName: 'dbo',
pureName: 'Customer',
});
const writer = await dbgateApi.csvWriter({ fileName: 'Customer.csv' });
const writer = await dbgatePluginCsv.shellApi.writer({ fileName: 'Customer.csv' });
await dbgateApi.copyStream(reader, writer);
console.log('Finished job script');
@@ -88,36 +92,11 @@ Imports data into table. Options are optional, default values are false.
});
```
### dbgateApi.csvReader
Reads CSV file
```js
const reader = await dbgateApi.csvReader({
fileName: '/home/root/test.csv',
encoding: 'utf-8',
header: true,
delimiter: ',',
quoted: false,
limitRows: null
});
```
### dbgateApi.csvWriter
Writes CSV file
```js
const reader = await dbgateApi.csvWriter({
fileName: '/home/root/test.csv',
encoding: 'utf-8',
header: true,
delimiter: ',',
quoted: false
});
```
### dbgateApi.jsonLinesReader
Reads JSON lines data file. On first line could be structure. Every line contains one row as JSON serialized object.
```js
const reader = await dbgateApi.jsonLinesReader({
fileName: '/home/root/test.jsonl',
fileName: 'test.jsonl',
encoding: 'utf-8',
header: true,
limitRows: null
@@ -128,19 +107,8 @@ Reads JSON lines data file. On first line could be structure. Every line contain
Writes JSON lines data file. On first line could be structure. Every line contains one row as JSON serialized object.
```js
const reader = await dbgateApi.jsonLinesWriter({
fileName: '/home/root/test.jsonl',
fileName: 'test.jsonl',
encoding: 'utf-8',
header: true
});
```
### dbgateApi.excelSheetReader
Reads tabular data from one sheet in MS Excel file.
```js
const reader = await dbgateApi.excelSheetReader({
fileName: '/home/root/test.xlsx',
sheetName: 'Album',
limitRows: null
});
```
+5 -2
View File
@@ -1,7 +1,7 @@
{
"name": "dbgate-api",
"main": "src/index.js",
"version": "1.0.5",
"version": "1.0.7",
"homepage": "https://dbgate.org/",
"repository": {
"type": "git",
@@ -38,11 +38,14 @@
"lodash": "^4.17.15",
"ncp": "^2.0.0",
"nedb-promises": "^4.0.1",
"tar": "^6.0.5"
"node-cron": "^2.0.3",
"tar": "^6.0.5",
"uuid": "^3.4.0"
},
"scripts": {
"start": "nodemon src/index.js",
"start:portal": "env-cmd nodemon src/index.js",
"start:covid": "env-cmd -f .covid-env nodemon src/index.js",
"ts": "tsc",
"build": "webpack"
},
+26 -12
View File
@@ -1,20 +1,34 @@
const currentVersion = require('../currentVersion');
module.exports = {
get_meta: 'get',
async get() {
const toolbarButtons = process.env.TOOLBAR;
const toolbar = toolbarButtons
? toolbarButtons.split(',').map((name) => ({
name,
icon: process.env[`ICON_${name}`],
title: process.env[`TITLE_${name}`],
page: process.env[`PAGE_${name}`],
}))
: null;
const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
// const toolbarButtons = process.env.TOOLBAR;
// const toolbar = toolbarButtons
// ? toolbarButtons.split(',').map((name) => ({
// name,
// icon: process.env[`ICON_${name}`],
// title: process.env[`TITLE_${name}`],
// page: process.env[`PAGE_${name}`],
// }))
// : null;
// const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null;
const singleDatabase =
process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE
? {
conid: process.env.SINGLE_CONNECTION,
database: process.env.SINGLE_DATABASE,
}
: null;
return {
runAsPortal: !!process.env.CONNECTIONS,
toolbar,
startupPages,
// toolbar,
// startupPages,
singleDatabase,
permissions,
...currentVersion,
};
},
};
+83
View File
@@ -0,0 +1,83 @@
const fs = require('fs-extra');
const path = require('path');
const { filesdir } = require('../utility/directories');
const hasPermission = require('../utility/hasPermission');
const socket = require('../utility/socket');
const scheduler = require('./scheduler');
function serialize(format, data) {
if (format == 'text') return data;
if (format == 'json') return JSON.stringify(data);
throw new Error(`Invalid format: ${format}`);
}
function deserialize(format, text) {
if (format == 'text') return text;
if (format == 'json') return JSON.parse(text);
throw new Error(`Invalid format: ${format}`);
}
module.exports = {
list_meta: 'get',
async list({ folder }) {
if (!hasPermission(`files/${folder}/read`)) return [];
const dir = path.join(filesdir(), folder);
if (!(await fs.exists(dir))) return [];
const files = (await fs.readdir(dir)).map((file) => ({ folder, file }));
return files;
},
delete_meta: 'post',
async delete({ folder, file }) {
if (!hasPermission(`files/${folder}/write`)) return;
await fs.unlink(path.join(filesdir(), folder, file));
socket.emitChanged(`files-changed-${folder}`);
},
rename_meta: 'post',
async rename({ folder, file, newFile }) {
if (!hasPermission(`files/${folder}/write`)) return;
await fs.rename(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
socket.emitChanged(`files-changed-${folder}`);
},
load_meta: 'post',
async load({ folder, file, format }) {
if (!hasPermission(`files/${folder}/read`)) return null;
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
return deserialize(format, text);
},
save_meta: 'post',
async save({ folder, file, data, format }) {
if (!hasPermission(`files/${folder}/write`)) return;
const dir = path.join(filesdir(), folder);
if (!(await fs.exists(dir))) {
await fs.mkdir(dir);
}
await fs.writeFile(path.join(dir, file), serialize(format, data));
socket.emitChanged(`files-changed-${folder}`);
if (folder == 'shell') {
scheduler.reload();
}
},
favorites_meta: 'get',
async favorites() {
if (!hasPermission(`files/favorites/read`)) return [];
const dir = path.join(filesdir(), 'favorites');
if (!(await fs.exists(dir))) return [];
const files = await fs.readdir(dir);
const res = [];
for (const file of files) {
const filePath = path.join(dir, file);
const text = await fs.readFile(filePath, { encoding: 'utf-8' });
res.push({
file,
folder: 'favorites',
...JSON.parse(text),
});
}
return res;
},
};
+4
View File
@@ -5,6 +5,7 @@ const { pluginsdir, datadir } = require('../utility/directories');
const socket = require('../utility/socket');
const requirePlugin = require('../shell/requirePlugin');
const downloadPackage = require('../utility/downloadPackage');
const hasPermission = require('../utility/hasPermission');
// async function loadPackageInfo(dir) {
// const readmeFile = path.join(dir, 'README.md');
@@ -106,6 +107,7 @@ module.exports = {
install_meta: 'post',
async install({ packageName }) {
if (!hasPermission(`plugins/install`)) return;
const dir = path.join(pluginsdir(), packageName);
if (!(await fs.exists(dir))) {
await downloadPackage(packageName, dir);
@@ -115,6 +117,7 @@ module.exports = {
uninstall_meta: 'post',
async uninstall({ packageName }) {
if (!hasPermission(`plugins/install`)) return;
const dir = path.join(pluginsdir(), packageName);
await fs.rmdir(dir, { recursive: true });
socket.emitChanged(`installed-plugins-changed`);
@@ -139,6 +142,7 @@ module.exports = {
}
for (const packageName of preinstallPlugins) {
if (this.removedPlugins.includes(packageName)) continue;
if (installed.find((x) => x.name == packageName)) continue;
try {
console.log('Preinstalling plugin', packageName);
await this.install({ packageName });
+1
View File
@@ -91,6 +91,7 @@ module.exports = {
fs.mkdirSync(directory);
const pluginNames = fs.readdirSync(pluginsdir());
console.log(`RUNNING SCRIPT ${scriptFile}`);
// const subprocess = fork(scriptFile, ['--checkParent', '--max-old-space-size=8192'], {
const subprocess = fork(scriptFile, ['--checkParent'], {
cwd: directory,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
+43
View File
@@ -0,0 +1,43 @@
const { filesdir } = require('../utility/directories');
const fs = require('fs-extra');
const path = require('path');
const cron = require('node-cron');
const runners = require('./runners');
const hasPermission = require('../utility/hasPermission');
const scheduleRegex = /\s*\/\/\s*@schedule\s+([^\n]+)\n/;
module.exports = {
tasks: [],
async unload() {
this.tasks.forEach((x) => x.destroy());
this.tasks = [];
},
async processFile(file) {
const text = await fs.readFile(file, { encoding: 'utf-8' });
const match = text.match(scheduleRegex);
if (!match) return;
const pattern = match[1];
if (!cron.validate(pattern)) return;
console.log(`Schedule script ${file} with pattern ${pattern}`);
const task = cron.schedule(pattern, () => runners.start({ script: text }));
this.tasks.push(task);
},
async reload() {
if (!hasPermission('files/shell/read')) return;
const shellDir = path.join(filesdir(), 'shell');
await this.unload();
if (!(await fs.exists(shellDir))) return;
const files = await fs.readdir(shellDir);
for (const file of files) {
await this.processFile(path.join(shellDir, file));
}
},
async _init() {
this.reload();
},
};
@@ -19,9 +19,6 @@ module.exports = {
existing.status = status;
socket.emitChanged(`server-status-changed`);
},
handle_error(conid, { error }) {
console.log(`Error in server connection ${conid}: ${error}`);
},
handle_ping() {},
async ensureOpened(conid) {
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
version: '3.8.6',
buildTime: '2020-12-10T11:14:01.053Z'
};
+7 -3
View File
@@ -23,6 +23,8 @@ const config = require('./controllers/config');
const archive = require('./controllers/archive');
const uploads = require('./controllers/uploads');
const plugins = require('./controllers/plugins');
const files = require('./controllers/files');
const scheduler = require('./controllers/scheduler');
const { rundir } = require('./utility/directories');
@@ -67,10 +69,12 @@ function start(argument = null) {
useController(app, '/archive', archive);
useController(app, '/uploads', uploads);
useController(app, '/plugins', plugins);
useController(app, '/files', files);
useController(app, '/scheduler', scheduler);
if (process.env.PAGES_DIRECTORY) {
app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
}
// if (process.env.PAGES_DIRECTORY) {
// app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
// }
app.use('/runners/data', express.static(rundir()));
@@ -96,8 +96,11 @@ function start() {
process.on('message', async (message) => {
try {
await handleMessage(message);
} catch (e) {
process.send({ msgtype: 'error', error: e.message });
} catch (err) {
setStatus({
name: 'error',
message: err.message,
});
}
});
}
+15
View File
@@ -0,0 +1,15 @@
const path = require('path');
const uuidv1 = require('uuid/v1');
const { uploadsdir } = require('../utility/directories');
const { downloadFile } = require('../utility/downloader');
async function download(url) {
if (url && url.match(/(^http:\/\/)|(^https:\/\/)/)) {
const tmpFile = path.join(uploadsdir(), uuidv1());
await downloadFile(url, tmpFile);
return tmpFile;
}
return url;
}
module.exports = download;
+19
View File
@@ -0,0 +1,19 @@
const goSplit = require('../utility/goSplit');
const requireEngineDriver = require('../utility/requireEngineDriver');
async function executeQuery({ connection, sql }) {
console.log(`Execute query ${sql}`);
const driver = requireEngineDriver(connection);
const pool = await driver.connect(connection);
console.log(`Connected.`);
for (const sqlItem of goSplit(sql)) {
console.log('Executing query', sqlItem);
await driver.query(pool, sqlItem);
}
console.log(`Query finished`);
}
module.exports = executeQuery;
+6
View File
@@ -14,6 +14,9 @@ const collectorWriter = require('./collectorWriter');
const finalizer = require('./finalizer');
const registerPlugins = require('./registerPlugins');
const requirePlugin = require('./requirePlugin');
const download = require('./download');
const executeQuery = require('./executeQuery');
const loadFile = require('./loadFile');
const dbgateApi = {
queryReader,
@@ -30,7 +33,10 @@ const dbgateApi = {
archiveReader,
collectorWriter,
finalizer,
download,
registerPlugins,
executeQuery,
loadFile,
};
requirePlugin.initialize(dbgateApi);
+10
View File
@@ -0,0 +1,10 @@
const fs = require('fs-extra');
const path = require('path');
const { filesdir } = require('../utility/directories');
async function loadFile(file) {
const text = await fs.readFile(path.join(filesdir(), file), { encoding: 'utf-8' });
return text;
}
module.exports = loadFile;
+5 -3
View File
@@ -1,4 +1,4 @@
const { quoteFullName } = require('dbgate-tools');
const { quoteFullName, fullNameToString } = require('dbgate-tools');
const requireEngineDriver = require('../utility/requireEngineDriver');
async function tableReader({ connection, pureName, schemaName }) {
@@ -11,13 +11,15 @@ async function tableReader({ connection, pureName, schemaName }) {
const table = await driver.analyseSingleObject(pool, fullName, 'tables');
const query = `select * from ${quoteFullName(driver.dialect, fullName)}`;
if (table) {
console.log(`Reading table ${table.pureName}`);
// @ts-ignore
console.log(`Reading table ${fullNameToString(table)}`);
// @ts-ignore
return await driver.readQuery(pool, query, table);
}
const view = await driver.analyseSingleObject(pool, fullName, 'views');
if (view) {
console.log(`Reading view ${view.pureName}`);
// @ts-ignore
console.log(`Reading view ${fullNameToString(view)}`);
// @ts-ignore
return await driver.readQuery(pool, query, view);
}
+3 -2
View File
@@ -1,7 +1,8 @@
const requireEngineDriver = require("../utility/requireEngineDriver");
const { fullNameToString } = require('dbgate-tools');
const requireEngineDriver = require('../utility/requireEngineDriver');
async function tableWriter({ connection, schemaName, pureName, ...options }) {
console.log(`Write table ${schemaName}.${pureName}`);
console.log(`Writing table ${fullNameToString({ schemaName, pureName })}`);
const driver = requireEngineDriver(connection);
const pool = await driver.connect(connection);
+2
View File
@@ -37,6 +37,7 @@ const rundir = dirFunc('run', true);
const uploadsdir = dirFunc('uploads', true);
const pluginsdir = dirFunc('plugins');
const archivedir = dirFunc('archive');
const filesdir = dirFunc('files');
module.exports = {
datadir,
@@ -46,4 +47,5 @@ module.exports = {
archivedir,
ensureDirectory,
pluginsdir,
filesdir,
};
+2 -14
View File
@@ -8,6 +8,7 @@ const zlib = require('zlib');
const tar = require('tar');
const ncp = require('ncp').ncp;
const { uploadsdir } = require('./directories');
const { downloadFile } = require('./downloader');
function extractTarball(tmpFile, destination) {
return new Promise((resolve, reject) => {
@@ -19,13 +20,6 @@ function extractTarball(tmpFile, destination) {
});
}
function saveStreamToFile(pipedStream, fileName) {
return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(fileName);
fileStream.on('close', () => resolve());
pipedStream.pipe(fileStream);
});
}
function copyDirectory(source, target) {
return new Promise((resolve, reject) => {
@@ -46,13 +40,7 @@ async function downloadPackage(packageName, directory) {
const tarball = infoResp.data.versions[latest].dist.tarball;
const tmpFile = path.join(uploadsdir(), uuidv1() + '.tgz');
console.log(`Downloading tarball ${tarball} into ${tmpFile}`);
const tarballResp = await axios.default({
method: 'get',
url: tarball,
responseType: 'stream',
});
await saveStreamToFile(tarballResp.data, tmpFile);
await downloadFile(tarball, tmpFile);
const tmpDir = path.join(uploadsdir(), uuidv1());
fs.mkdirSync(tmpDir);
await extractTarball(tmpFile, tmpDir);
+25
View File
@@ -0,0 +1,25 @@
const axios = require('axios');
const fs = require('fs');
function saveStreamToFile(pipedStream, fileName) {
return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(fileName);
fileStream.on('close', () => resolve());
pipedStream.pipe(fileStream);
});
}
async function downloadFile(url, file) {
console.log(`Downloading ${url} into ${file}`);
const tarballResp = await axios.default({
method: 'get',
url,
responseType: 'stream',
});
await saveStreamToFile(tarballResp.data, file);
}
module.exports = {
saveStreamToFile,
downloadFile,
};
+12
View File
@@ -0,0 +1,12 @@
const { compilePermissions, testPermission } = require('dbgate-tools');
let compiled = undefined;
function hasPermission(tested) {
if (compiled === undefined) {
compiled = compilePermissions(process.env.PERMISSIONS);
}
return testPermission(tested, compiled);
}
module.exports = hasPermission;
+2 -1
View File
@@ -433,9 +433,10 @@ export abstract class GridDisplay {
return sql;
}
getExportQuery() {
getExportQuery(postprocessSelect = null) {
const select = this.createSelect({ isExport: true });
if (!select) return null;
if (postprocessSelect) postprocessSelect(select);
const sql = treeToSql(this.driver, select, dumpSqlSelect);
return sql;
}
+2
View File
@@ -0,0 +1,2 @@
version-tag-prefix packages-sqlitree-v
version-git-message "packages-sqlitree v%s"
+2 -2
View File
@@ -8,7 +8,7 @@ dbgate-sqltree hold query definition in RAW JSON objects.
```javascript
const { treeToSql, dumpSqlSelect } = require("dbgate-sqltree");
const engines = require("dbgate-engines");
const dbgatePluginMysql = require("dbgate-plugin-mysql");
const select = {
commandType: "select",
@@ -32,7 +32,7 @@ const select = {
],
};
const sql = treeToSql(engines("mysql"), select, dumpSqlSelect);
const sql = treeToSql(dbgatePluginMysql.driver, select, dumpSqlSelect);
console.log("Generated query:", sql);
```
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "1.0.3",
"version": "1.0.4",
"name": "dbgate-sqltree",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
+4 -1
View File
@@ -7,7 +7,7 @@ import { dumpSqlCondition } from './dumpSqlCondition';
export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
dmp.put('^select ');
if (cmd.topRecords) {
dmp.put('^top %s ', cmd.topRecords);
if (!dmp.dialect.rangeSelect || dmp.dialect.offsetFetchRangeSyntax) dmp.put('^top %s ', cmd.topRecords);
}
if (cmd.distinct) {
dmp.put('^distinct ');
@@ -51,6 +51,9 @@ export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
dmp.put('^limit %s ^offset %s ', cmd.range.limit, cmd.range.offset);
}
}
if (cmd.topRecords) {
if (dmp.dialect.rangeSelect && !dmp.dialect.offsetFetchRangeSyntax) dmp.put('^limit %s ', cmd.topRecords);
}
}
export function dumpSqlUpdate(dmp: SqlDumper, cmd: Update) {
+2
View File
@@ -0,0 +1,2 @@
version-tag-prefix packages-tools-v
version-git-message "packages-tools v%s"
+3 -2
View File
@@ -1,5 +1,5 @@
{
"version": "1.0.4",
"version": "1.0.5",
"name": "dbgate-tools",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
@@ -19,7 +19,8 @@
"prepare": "yarn build",
"build": "tsc",
"start": "tsc --watch",
"test": "jest"
"test": "jest",
"prepublishOnly": "yarn build"
},
"files": [
"lib"
@@ -43,7 +43,7 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
await driver.query(pool, `TRUNCATE TABLE ${fullNameQuoted}`);
}
this.columnNames = _intersection(
writable.columnNames = _intersection(
structure.columns.map((x) => x.columnName),
writable.structure.columns.map((x) => x.columnName)
);
@@ -56,14 +56,14 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
const dmp = driver.createDumper();
dmp.putRaw(`INSERT INTO ${fullNameQuoted} (`);
dmp.putCollection(',', this.columnNames, (col) => dmp.putRaw(driver.dialect.quoteIdentifier(col)));
dmp.putCollection(',', writable.columnNames, (col) => dmp.putRaw(driver.dialect.quoteIdentifier(col)));
dmp.putRaw(')\n VALUES\n');
let wasRow = false;
for (const row of rows) {
if (wasRow) dmp.putRaw(',\n');
dmp.putRaw('(');
dmp.putCollection(',', this.columnNames, (col) => dmp.putValue(row[col]));
dmp.putCollection(',', writable.columnNames, (col) => dmp.putValue(row[col]));
dmp.putRaw(')');
wasRow = true;
}
+1
View File
@@ -6,3 +6,4 @@ export * from './createBulkInsertStreamBase';
export * from './DatabaseAnalyser';
export * from './driverBase';
export * from './SqlDumper';
export * from './testPermission';
+9
View File
@@ -18,6 +18,15 @@ export function extractShellApiPlugins(functionName, props): string[] {
return res;
}
export function extractPackageName(name): string {
if (!name) return null;
const nsMatch = name.match(/^([^@]+)@([^@]+)/);
if (nsMatch) {
return nsMatch[2];
}
return null;
}
export function extractShellApiFunctionName(functionName) {
const nsMatch = functionName.match(/^([^@]+)@([^@]+)/);
if (nsMatch) {
+16
View File
@@ -0,0 +1,16 @@
import _escapeRegExp from 'lodash/escapeRegExp';
import _isString from 'lodash/isString';
export function compilePermissions(permissions: string[] | string) {
if (!permissions) return null;
if (_isString(permissions)) permissions = permissions.split(',');
return permissions.map((x) => new RegExp('^' + _escapeRegExp(x).replace(/\\\*/g, '.*') + '$'));
}
export function testPermission(tested: string, permissions: RegExp[]) {
if (!permissions) return true;
for (const permission of permissions) {
if (tested.match(permission)) return true;
}
return false;
}
+2
View File
@@ -0,0 +1,2 @@
version-tag-prefix packages-types-v
version-git-message "packages-types v%s"
+1
View File
@@ -7,6 +7,7 @@ Typescript definitions for DbGate app
- dumper.d.ts - SQL dumper - dump SQL commands independed on DB engine
- engines.d.ts - definition of SQL engine driver
- query.d.ts - query results definition
- extensions.d.ts - plugin related definitions
## Installation
+4 -3
View File
@@ -1,4 +1,4 @@
import { EngineDriver } from "./engines";
import { EngineDriver } from './engines';
export interface FileFormatDefinition {
storageType: string;
@@ -7,9 +7,10 @@ export interface FileFormatDefinition {
readerFunc?: string;
writerFunc?: string;
args?: any[];
addFilesToSourceList?: (
addFileToSourceList?: (
file: {
full: string;
fileName: string;
shortName: string;
},
newSources: string[],
newValues: {
+2 -5
View File
@@ -1,20 +1,17 @@
{
"version": "1.0.1",
"version": "1.0.2",
"name": "dbgate-types",
"homepage": "https://dbgate.org/",
"repository": {
"type": "git",
"url": "https://github.com/dbshell/dbgate.git"
},
},
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
"author": "Jan Prochazka",
"license": "GPL",
"keywords": [
"dbgate"
],
"types": "index.d.ts",
"main": "",
"typeScriptVersion": "2.8"
+5 -2
View File
@@ -10,21 +10,24 @@
"@testing-library/user-event": "^7.1.2",
"ace-builds": "^1.4.8",
"axios": "^0.19.0",
"chart.js": "^2.9.4",
"cross-env": "^6.0.3",
"dbgate-datalib": "^1.0.0",
"dbgate-sqltree": "^1.0.0",
"dbgate-tools": "^1.0.0",
"eslint": "^6.8.0",
"eslint-plugin-react": "^7.17.0",
"formik": "^2.1.0",
"json-stable-stringify": "^1.0.1",
"localforage": "^1.9.0",
"markdown-to-jsx": "^7.1.0",
"randomcolor": "^0.6.2",
"react": "^16.12.0",
"react-ace": "^8.0.0",
"react-chartjs-2": "^2.11.1",
"react-dom": "^16.12.0",
"react-dropzone": "^11.2.3",
"react-helmet": "^6.1.0",
"react-json-view": "^1.19.1",
"react-markdown": "^5.0.3",
"react-modal": "^3.11.1",
"react-scripts": "3.3.0",
"react-select": "^3.1.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 137 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "DbGate",
"name": "DbGate database tool",
"icons": [
{
"src": "favicon.ico",
+21 -21
View File
@@ -5,7 +5,6 @@ import {
CurrentWidgetProvider,
CurrentDatabaseProvider,
OpenedTabsProvider,
SavedSqlFilesProvider,
OpenedConnectionsProvider,
LeftPanelWidthProvider,
CurrentArchiveProvider,
@@ -18,6 +17,7 @@ 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 (
@@ -25,28 +25,28 @@ function App() {
<CurrentDatabaseProvider>
<SocketProvider>
<OpenedTabsProvider>
<SavedSqlFilesProvider>
<OpenedConnectionsProvider>
<LeftPanelWidthProvider>
<ConnectionsPinger>
<PluginsProvider>
<ExtensionsProvider>
<ModalLayerProvider>
<CurrentArchiveProvider>
<CurrentThemeProvider>
<UploadsProvider>
<OpenedConnectionsProvider>
<LeftPanelWidthProvider>
<ConnectionsPinger>
<PluginsProvider>
<ExtensionsProvider>
<CurrentArchiveProvider>
<CurrentThemeProvider>
<UploadsProvider>
<ModalLayerProvider>
<MenuLayerProvider>
<ThemeHelmet />
<Screen />
</UploadsProvider>
</CurrentThemeProvider>
</CurrentArchiveProvider>
</ModalLayerProvider>
</ExtensionsProvider>
</PluginsProvider>
</ConnectionsPinger>
</LeftPanelWidthProvider>
</OpenedConnectionsProvider>
</SavedSqlFilesProvider>
</MenuLayerProvider>
</ModalLayerProvider>
</UploadsProvider>
</CurrentThemeProvider>
</CurrentArchiveProvider>
</ExtensionsProvider>
</PluginsProvider>
</ConnectionsPinger>
</LeftPanelWidthProvider>
</OpenedConnectionsProvider>
</OpenedTabsProvider>
</SocketProvider>
</CurrentDatabaseProvider>
+6 -1
View File
@@ -15,6 +15,8 @@ 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 from './utility/ErrorBoundary';
const BodyDiv = styled.div`
position: fixed;
@@ -112,7 +114,9 @@ export default function Screen() {
</IconBar>
{!!currentWidget && (
<LeftPanel theme={theme}>
<WidgetContainer />
<ErrorBoundary>
<WidgetContainer />
</ErrorBoundary>
</LeftPanel>
)}
{!!currentWidget && (
@@ -132,6 +136,7 @@ export default function Screen() {
<StatusBar />
</StausBarContainer>
<ModalLayer />
<MenuLayer />
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
</div>
+8 -5
View File
@@ -3,6 +3,7 @@ import _ from 'lodash';
import styled from 'styled-components';
import tabs from './tabs';
import { useOpenedTabs } from './utility/globalState';
import ErrorBoundary from './utility/ErrorBoundary';
const TabContainer = styled.div`
position: absolute;
@@ -11,7 +12,7 @@ const TabContainer = styled.div`
right: 0;
bottom: 0;
display: flex;
visibility: ${props =>
visibility: ${(props) =>
// @ts-ignore
props.tabVisible ? 'visible' : 'hidden'};
`;
@@ -34,10 +35,10 @@ export default function TabContent({ toolbarPortalRef }) {
// cleanup closed tabs
if (_.difference(_.keys(mountedTabs), _.map(files, 'tabid')).length > 0) {
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find(x => x.tabid == k)));
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find((x) => x.tabid == k)));
}
const selectedTab = files.find(x => x.selected);
const selectedTab = files.find((x) => x.selected);
if (selectedTab) {
const { tabid } = selectedTab;
if (tabid && !mountedTabs[tabid])
@@ -47,13 +48,15 @@ export default function TabContent({ toolbarPortalRef }) {
});
}
return _.keys(mountedTabs).map(tabid => {
return _.keys(mountedTabs).map((tabid) => {
const { TabComponent, props } = mountedTabs[tabid];
const tabVisible = tabid == (selectedTab && selectedTab.tabid);
return (
// @ts-ignore
<TabContainer key={tabid} tabVisible={tabVisible}>
<TabComponent {...props} tabid={tabid} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} />
<ErrorBoundary>
<TabComponent {...props} tabid={tabid} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} />
</ErrorBoundary>
</TabContainer>
);
});
+2 -1
View File
@@ -4,11 +4,11 @@ import styled from 'styled-components';
import { DropDownMenuItem, DropDownMenuDivider } from './modals/DropDownMenu';
import { useOpenedTabs, useSetOpenedTabs, useCurrentDatabase, useSetCurrentDatabase } from './utility/globalState';
import { showMenu } from './modals/DropDownMenu';
import { getConnectionInfo } from './utility/metadataLoaders';
import { FontIcon } from './icons';
import useTheme from './theme/useTheme';
import usePropsCompare from './utility/usePropsCompare';
import { useShowMenu } from './modals/showMenu';
// const files = [
// { name: 'app.js' },
@@ -126,6 +126,7 @@ function getDbIcon(key) {
export default function TabsPanel() {
// const formatDbKey = (conid, database) => `${database}-${conid}`;
const theme = useTheme();
const showMenu = useShowMenu();
const tabs = useOpenedTabs();
const setOpenedTabs = useSetOpenedTabs();
@@ -4,9 +4,8 @@ import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import { showMenu } from '../modals/DropDownMenu';
import { useShowMenu } from '../modals/showMenu';
import useTheme from '../theme/useTheme';
import { useSetOpenedTabs, useAppObjectParams } from '../utility/globalState';
const AppObjectDiv = styled.div`
padding: 5px;
@@ -18,11 +17,6 @@ const AppObjectDiv = styled.div`
font-weight: ${(props) => (props.isBold ? 'bold' : 'normal')};
`;
const AppObjectSpan = styled.span`
white-space: nowrap;
font-weight: ${(props) => (props.isBold ? 'bold' : 'normal')};
`;
const IconWrap = styled.span`
margin-right: 5px;
`;
@@ -32,49 +26,47 @@ const StatusIconWrap = styled.span`
`;
const ExtInfoWrap = styled.span`
font-weight: normal;
margin-left: 5px;
font-weight: normal;
margin-left: 5px;
color: ${(props) => props.theme.left_font3};
`;
export function AppObjectCore({
title,
icon,
Menu,
data,
makeAppObj,
onClick,
isBold,
isBusy,
component = 'div',
prefix = null,
statusIcon,
extInfo,
statusTitle,
onClick = undefined,
onClick2 = undefined,
onClick3 = undefined,
isBold = undefined,
isBusy = undefined,
prefix = undefined,
statusIcon = undefined,
extInfo = undefined,
statusTitle = undefined,
Menu = undefined,
...other
}) {
const appObjectParams = useAppObjectParams();
const theme = useTheme();
const showMenu = useShowMenu();
const handleContextMenu = (event) => {
if (!Menu) return;
event.preventDefault();
showMenu(event.pageX, event.pageY, <Menu data={data} makeAppObj={makeAppObj} {...appObjectParams} />);
showMenu(event.pageX, event.pageY, <Menu data={data} />);
};
const Component = component == 'div' ? AppObjectDiv : AppObjectSpan;
let bold = false;
if (_.isFunction(isBold)) bold = isBold(appObjectParams);
else bold = !!isBold;
return (
<Component
<AppObjectDiv
onContextMenu={handleContextMenu}
onClick={onClick ? () => onClick(data) : undefined}
isBold={bold}
onClick={() => {
if (onClick) onClick(data);
if (onClick2) onClick2(data);
if (onClick3) onClick3(data);
}}
theme={theme}
isBold={isBold}
{...other}
>
{prefix}
@@ -86,12 +78,6 @@ export function AppObjectCore({
</StatusIconWrap>
)}
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
</Component>
</AppObjectDiv>
);
}
export function AppObjectControl({ data, makeAppObj, component = 'div' }) {
const appObjectParams = useAppObjectParams();
const appobj = makeAppObj(data, appObjectParams);
return <AppObjectCore {...appobj} data={data} makeAppObj={makeAppObj} component={component} />;
}
+44 -61
View File
@@ -1,7 +1,5 @@
import React from 'react';
import _ from 'lodash';
import { AppObjectCore } from './AppObjects';
import { useSetOpenedTabs, useAppObjectParams } from '../utility/globalState';
import styled from 'styled-components';
import { ExpandIcon } from '../icons';
import useTheme from '../theme/useTheme';
@@ -31,53 +29,45 @@ const GroupDiv = styled.div`
font-weight: bold;
`;
function AppObjectListItem({ makeAppObj, data, filter, appobj, onObjectClick, SubItems }) {
function AppObjectListItem({
AppObjectComponent,
data,
filter,
onObjectClick,
isExpandable,
SubItems,
getCommonProps,
}) {
const [isExpanded, setIsExpanded] = React.useState(false);
const [isHover, setIsHover] = React.useState(false);
const expandable = data && isExpandable && isExpandable(data);
React.useEffect(() => {
if (!appobj.isExpandable) {
// if (data._id == '6pOY2iFY8Gsq7mk6') console.log('COLLAPSE1');
if (!expandable) {
setIsExpanded(false);
}
}, [appobj && appobj.isExpandable]);
}, [expandable]);
// const { matcher } = appobj;
// if (matcher && !matcher(filter)) return null;
let commonProps = {
prefix: SubItems ? (
<ExpandIconHolder2>
{expandable ? <ExpandIcon isExpanded={isExpanded} /> : <ExpandIcon isBlank />}
</ExpandIconHolder2>
) : null,
};
if (onObjectClick)
appobj = {
...appobj,
onClick: onObjectClick,
};
if (SubItems) {
const oldClick = appobj.onClick;
appobj = {
...appobj,
onClick: () => {
if (oldClick) oldClick();
// if (data._id == '6pOY2iFY8Gsq7mk6') console.log('COLLAPSE2');
setIsExpanded((v) => !v);
},
};
commonProps.onClick2 = () => setIsExpanded((v) => !v);
}
if (onObjectClick) {
commonProps.onClick3 = onObjectClick;
}
let res = (
<AppObjectCore
data={data}
makeAppObj={makeAppObj}
{...appobj}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
prefix={
SubItems ? (
<ExpandIconHolder2>
{appobj.isExpandable ? <ExpandIcon isExpanded={isExpanded} /> : <ExpandIcon isBlank />}
</ExpandIconHolder2>
) : null
}
/>
);
if (getCommonProps) {
commonProps = { ...commonProps, ...getCommonProps(data) };
}
let res = <AppObjectComponent data={data} commonProps={commonProps} />;
if (SubItems && isExpanded) {
res = (
<>
@@ -93,16 +83,10 @@ function AppObjectListItem({ makeAppObj, data, filter, appobj, onObjectClick, Su
function AppObjectGroup({ group, items }) {
const [isExpanded, setIsExpanded] = React.useState(true);
const [isHover, setIsHover] = React.useState(false);
const theme = useTheme();
return (
<>
<GroupDiv
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
onClick={() => setIsExpanded(!isExpanded)}
theme={theme}
>
<GroupDiv onClick={() => setIsExpanded(!isExpanded)} theme={theme}>
<ExpandIconHolder>
<ExpandIcon isExpanded={isExpanded} />
</ExpandIconHolder>
@@ -115,36 +99,36 @@ function AppObjectGroup({ group, items }) {
export function AppObjectList({
list,
makeAppObj,
AppObjectComponent,
SubItems = undefined,
onObjectClick = undefined,
filter = undefined,
groupFunc = undefined,
groupOrdered = undefined,
isExpandable = undefined,
getCommonProps = undefined,
}) {
const appObjectParams = useAppObjectParams();
const createComponent = (data, appobj) => (
const createComponent = (data) => (
<AppObjectListItem
key={appobj.key}
appobj={appobj}
makeAppObj={makeAppObj}
key={AppObjectComponent.extractKey(data)}
AppObjectComponent={AppObjectComponent}
data={data}
filter={filter}
onObjectClick={onObjectClick}
SubItems={SubItems}
isExpandable={isExpandable}
getCommonProps={getCommonProps}
/>
);
if (groupFunc) {
const listGrouped = _.compact(
(list || []).map((data) => {
const appobj = makeAppObj(data, appObjectParams);
const { matcher } = appobj;
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
if (matcher && !matcher(filter)) return null;
const component = createComponent(data, appobj);
const group = groupFunc(appobj);
return { group, appobj, component };
const component = createComponent(data);
const group = groupFunc(data);
return { group, data, component };
})
);
const groups = _.groupBy(listGrouped, 'group');
@@ -154,9 +138,8 @@ export function AppObjectList({
}
return (list || []).map((data) => {
const appobj = makeAppObj(data, appObjectParams);
const { matcher } = appobj;
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
if (matcher && !matcher(filter)) return null;
return createComponent(data, appobj);
return createComponent(data);
});
}
@@ -1,13 +1,12 @@
import React from 'react';
import _ from 'lodash';
import moment from 'moment';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { openNewTab } from '../utility/common';
import { filterName } from 'dbgate-datalib';
import axios from '../utility/axios';
import { AppObjectCore } from './AppObjectCore';
import useOpenNewTab from '../utility/useOpenNewTab';
function openArchive(setOpenedTabs, fileName, folderName) {
openNewTab(setOpenedTabs, {
function openArchive(openNewTab, fileName, folderName) {
openNewTab({
title: fileName,
icon: 'img archive',
tooltip: `${folderName}\n${fileName}`,
@@ -19,23 +18,24 @@ function openArchive(setOpenedTabs, fileName, folderName) {
});
}
function Menu({ data, setOpenedTabs }) {
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(setOpenedTabs, data.fileName, data.folderName);
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(setOpenedTabs, {
openNewTab({
title: data.fileName,
icon: 'img archive',
tabComponent: 'FreeTableTab',
props: {
initialData: {
initialArgs: {
functionName: 'archiveReader',
props: {
fileName: data.fileName,
@@ -57,15 +57,19 @@ function Menu({ data, setOpenedTabs }) {
);
}
const archiveFileAppObject = () => ({ fileName, folderName }, { setOpenedTabs }) => {
const key = fileName;
const icon = 'img archive';
function ArchiveFileAppObject({ data, commonProps }) {
const { fileName, folderName } = data;
const openNewTab = useOpenNewTab();
const onClick = () => {
openArchive(setOpenedTabs, fileName, folderName);
openArchive(openNewTab, fileName, folderName);
};
const matcher = (filter) => filterName(filter, fileName);
return { title: fileName, key, icon, Menu, onClick, matcher };
};
return (
<AppObjectCore {...commonProps} data={data} title={fileName} icon="img archive" onClick={onClick} Menu={Menu} />
);
}
export default archiveFileAppObject;
ArchiveFileAppObject.extractKey = (data) => data.fileName;
ArchiveFileAppObject.createMatcher = ({ fileName }) => (filter) => filterName(filter, fileName);
export default ArchiveFileAppObject;
@@ -0,0 +1,34 @@
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;
@@ -2,8 +2,11 @@ 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';
function Menu({ data, setOpenedTabs }) {
function Menu({ data }) {
const setOpenedTabs = useSetOpenedTabs();
const handleDelete = () => {
setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
};
@@ -18,9 +21,9 @@ function Menu({ data, setOpenedTabs }) {
);
}
const closedTabAppObject = () => ({ tabid, props, selected, icon, title, closedTime, busy }, { setOpenedTabs }) => {
const key = tabid;
const isBold = !!selected;
function ClosedTabAppObject({ data, commonProps }) {
const { tabid, props, selected, icon, title, closedTime, busy } = data;
const setOpenedTabs = useSetOpenedTabs();
const onClick = () => {
setOpenedTabs((files) =>
@@ -32,7 +35,20 @@ const closedTabAppObject = () => ({ tabid, props, selected, icon, title, closedT
);
};
return { title: `${title} ${moment(closedTime).fromNow()}`, key, icon, isBold, onClick, isBusy: busy, Menu };
};
return (
<AppObjectCore
{...commonProps}
data={data}
title={`${title} ${moment(closedTime).fromNow()}`}
icon={icon}
isBold={!!selected}
onClick={onClick}
isBusy={busy}
Menu={Menu}
/>
);
}
export default closedTabAppObject;
ClosedTabAppObject.extractKey = (data) => data.tabid;
export default ClosedTabAppObject;
@@ -6,8 +6,18 @@ 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();
function Menu({ data, setOpenedConnections, openedConnections, config, showModal }) {
const handleEdit = () => {
showModal((modalState) => <ConnectionModal modalState={modalState} connection={data} />);
};
@@ -54,49 +64,56 @@ function Menu({ data, setOpenedConnections, openedConnections, config, showModal
);
}
const connectionAppObject = (flags) => (
{ _id, server, displayName, engine, status },
{ openedConnections, setOpenedConnections }
) => {
const title = displayName || server;
const key = _id;
const isExpandable = openedConnections.includes(_id);
const icon = 'img server';
const matcher = (filter) => filterName(filter, displayName, server);
const { boldCurrentDatabase } = flags || {};
const isBold = boldCurrentDatabase
? ({ currentDatabase }) => {
return _.get(currentDatabase, 'connection._id') == _id;
}
: null;
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) => [...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 green-ok';
else if (status.name == 'ok') statusIcon = 'img ok';
else statusIcon = 'img error';
if (status && status.name == 'error') {
statusTitle = status.message;
}
}
const extInfo = engine;
return {
title,
key,
icon,
Menu,
matcher,
isBold,
isExpandable,
onClick,
statusIcon,
statusTitle,
extInfo,
};
};
return (
<AppObjectCore
{...commonProps}
title={displayName || server}
icon="img server"
data={data}
statusIcon={statusIcon}
statusTitle={statusTitle}
extInfo={extInfo}
isBold={isBold}
onClick={onClick}
Menu={Menu}
/>
);
}
export default connectionAppObject;
ConnectionAppObject.extractKey = (data) => data._id;
ConnectionAppObject.createMatcher = ({ displayName, server }) => (filter) => filterName(filter, displayName, server);
export default ConnectionAppObject;
@@ -1,16 +1,25 @@
import React from 'react';
import _ from 'lodash';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { openNewTab } from '../utility/common';
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, setOpenedTabs, showModal, extensions }) {
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(setOpenedTabs, {
openNewTab({
title: 'Query',
icon: 'img sql-file',
tooltip,
@@ -29,8 +38,8 @@ function Menu({ data, setOpenedTabs, showModal, extensions }) {
initialValues={{
sourceStorageType: getDefaultFileFormat(extensions).storageType,
targetStorageType: 'database',
targetConnectionId: data.connection._id,
targetDatabaseName: data.name,
targetConnectionId: connection._id,
targetDatabaseName: name,
}}
/>
));
@@ -43,8 +52,8 @@ function Menu({ data, setOpenedTabs, showModal, extensions }) {
initialValues={{
targetStorageType: getDefaultFileFormat(extensions).storageType,
sourceStorageType: 'database',
sourceConnectionId: data.connection._id,
sourceDatabaseName: data.name,
sourceConnectionId: connection._id,
sourceDatabaseName: name,
}}
/>
));
@@ -59,20 +68,23 @@ function Menu({ data, setOpenedTabs, showModal, extensions }) {
);
}
const databaseAppObject = (flags) => ({ name, connection }) => {
const { boldCurrentDatabase } = flags || {};
const title = name;
const key = name;
const icon = 'img database';
const isBold = boldCurrentDatabase
? ({ currentDatabase }) => {
return (
_.get(currentDatabase, 'connection._id') == _.get(connection, '_id') && _.get(currentDatabase, 'name') == name
);
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
}
: null;
Menu={Menu}
/>
);
}
return { title, key, icon, Menu, isBold };
};
DatabaseAppObject.extractKey = (props) => props.name;
export default databaseAppObject;
export default DatabaseAppObject;
@@ -1,11 +1,16 @@
import _ from 'lodash';
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { openNewTab } from '../utility/common';
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';
const icons = {
tables: 'img table',
@@ -36,6 +41,10 @@ const menus = {
label: 'Open in free table editor',
isOpenFreeTable: true,
},
{
label: 'Open active chart',
isActiveChart: true,
},
],
views: [
{
@@ -62,6 +71,10 @@ const menus = {
label: 'Open structure',
tab: 'TableStructureTab',
},
{
label: 'Open active chart',
isActiveChart: true,
},
],
procedures: [
{
@@ -87,7 +100,7 @@ const defaultTabs = {
};
export async function openDatabaseObjectDetail(
setOpenedTabs,
openNewTab,
tabComponent,
sqlTemplate,
{ schemaName, pureName, conid, database, objectTypeField }
@@ -98,7 +111,7 @@ export async function openDatabaseObjectDetail(
pureName,
})}`;
openNewTab(setOpenedTabs, {
openNewTab({
title: pureName,
tooltip,
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
@@ -114,7 +127,18 @@ export async function openDatabaseObjectDetail(
});
}
function Menu({ data, makeAppObj, setOpenedTabs, showModal }) {
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) => (
@@ -136,12 +160,12 @@ function Menu({ data, makeAppObj, setOpenedTabs, showModal }) {
));
} else if (menu.isOpenFreeTable) {
const coninfo = await getConnectionInfo(data);
openNewTab(setOpenedTabs, {
openNewTab({
title: data.pureName,
icon: 'img free-table',
tabComponent: 'FreeTableTab',
props: {
initialData: {
initialArgs: {
functionName: 'tableReader',
props: {
connection: {
@@ -154,8 +178,29 @@ function Menu({ data, makeAppObj, setOpenedTabs, showModal }) {
},
},
});
} 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 {
openDatabaseObjectDetail(setOpenedTabs, menu.tab, menu.sqlTemplate, data);
openDatabaseObjectDetail(openNewTab, menu.tab, menu.sqlTemplate, data);
}
}}
>
@@ -166,17 +211,12 @@ function Menu({ data, makeAppObj, setOpenedTabs, showModal }) {
);
}
const databaseObjectAppObject = () => (
{ conid, database, pureName, schemaName, objectTypeField },
{ setOpenedTabs }
) => {
const title = schemaName ? `${schemaName}.${pureName}` : pureName;
const key = title;
const icon = icons[objectTypeField];
// const Icon = (props) => getIconImage(icons[objectTypeField], props);
function DatabaseObjectAppObject({ data, commonProps }) {
const { conid, database, pureName, schemaName, objectTypeField } = data;
const openNewTab = useOpenNewTab();
const onClick = ({ schemaName, pureName }) => {
openDatabaseObjectDetail(
setOpenedTabs,
openNewTab,
defaultTabs[objectTypeField],
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
{
@@ -188,10 +228,22 @@ const databaseObjectAppObject = () => (
}
);
};
const matcher = (filter) => filterName(filter, pureName);
const groupTitle = _.startCase(objectTypeField);
return { title, key, icon, Menu, onClick, matcher, groupTitle };
};
return (
<AppObjectCore
{...commonProps}
data={data}
title={schemaName ? `${schemaName}.${pureName}` : pureName}
icon={icons[objectTypeField]}
onClick={onClick}
Menu={Menu}
/>
);
}
export default databaseObjectAppObject;
DatabaseObjectAppObject.extractKey = ({ schemaName, pureName }) =>
schemaName ? `${schemaName}.${pureName}` : pureName;
DatabaseObjectAppObject.createMatcher = ({ pureName }) => (filter) => filterName(filter, pureName);
export default DatabaseObjectAppObject;
@@ -0,0 +1,104 @@
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;
+10 -8
View File
@@ -1,13 +1,15 @@
import _ from 'lodash';
import React from 'react';
import { filterName } from 'dbgate-datalib';
import { AppObjectCore } from './AppObjectCore';
const macroAppObject = () => ({ name, type, title, group }, { setOpenedTabs }) => {
const key = name;
const icon = 'img macro';
const matcher = (filter) => filterName(filter, name, title);
const groupTitle = group;
function MacroAppObject({ data, commonProps }) {
const { name, type, title, group } = data;
return { title, key, icon, groupTitle, matcher };
};
return <AppObjectCore {...commonProps} data={data} title={title} icon={'img macro'} />;
}
export default macroAppObject;
MacroAppObject.extractKey = (data) => data.name;
MacroAppObject.createMatcher = ({ name, title }) => (filter) => filterName(filter, name, title);
export default MacroAppObject;
@@ -0,0 +1,253 @@
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 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>}
/>
);
}
[SavedSqlFileAppObject, SavedShellFileAppObject, SavedChartFileAppObject, SavedMarkdownFileAppObject].forEach((fn) => {
// @ts-ignore
fn.extractKey = (data) => data.file;
});
@@ -1,24 +0,0 @@
import React from 'react';
import _ from 'lodash';
import moment from 'moment';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import axios from '../utility/axios';
import { filterName } from 'dbgate-datalib';
function Menu({ data, setOpenedTabs }) {
const handleDelete = () => {
axios.post('archive/delete-folder', { folder: data.name });
};
return <>{data.name != 'default' && <DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>}</>;
}
const archiveFolderAppObject = () => ({ name }, { setOpenedTabs, currentArchive }) => {
const key = name;
const icon = 'img archive-folder';
const isBold = name == currentArchive;
const matcher = (filter) => filterName(filter, name);
return { title: name, key, icon, isBold, Menu, matcher };
};
export default archiveFolderAppObject;
@@ -1,15 +0,0 @@
/** @param columnProps {import('dbgate-types').ColumnInfo} */
function getColumnIcon(columnProps) {
if (columnProps.autoIncrement) return 'img autoincrement';
return 'img column';
}
/** @param columnProps {import('dbgate-types').ColumnInfo} */
export default function columnAppObject(columnProps, { setOpenedTabs }) {
const title = columnProps.columnName;
const key = title;
const icon = getColumnIcon(columnProps);
const isBold = columnProps.notNull;
return { title, key, icon, isBold };
}
@@ -1,15 +0,0 @@
/** @param props {import('dbgate-types').ConstraintInfo} */
function getConstraintIcon(props) {
if (props.constraintType == 'primaryKey') return 'img primary-key';
if (props.constraintType == 'foreignKey') return 'img foreign-key';
return null;
}
/** @param props {import('dbgate-types').ConstraintInfo} */
export default function constraintAppObject(props, { setOpenedTabs }) {
const title = props.constraintName;
const key = title;
const icon = getConstraintIcon(props);
return { title, key, icon };
}
@@ -1,41 +0,0 @@
import React from 'react';
import _ from 'lodash';
import { DropDownMenuItem } from '../modals/DropDownMenu';
function Menu({ data, setSavedSqlFiles }) {
const handleDelete = () => {
setSavedSqlFiles((files) => files.filter((x) => x.storageKey != data.storageKey));
};
return (
<>
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
</>
);
}
const savedSqlFileAppObject = () => ({ name, storageKey }, { setOpenedTabs, newQuery, openedTabs }) => {
const key = storageKey;
const title = name;
const icon = 'img sql-file';
const onClick = () => {
const existing = openedTabs.find((x) => x.props && x.props.storageKey == storageKey);
if (existing) {
setOpenedTabs(
openedTabs.map((x) => ({
...x,
selected: x == existing,
}))
);
} else {
newQuery({
title,
storageKey,
});
}
};
return { title, key, icon, onClick, Menu };
};
export default savedSqlFileAppObject;
+154
View File
@@ -0,0 +1,154 @@
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>
);
}
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton';
export default function ChartToolbar({ save, modelState, dispatchModel }) {
const hasPermission = useHasPermission();
return (
<>
{hasPermission('files/charts/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<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>
</>
);
}
+165
View File
@@ -0,0 +1,165 @@
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 || 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>
);
}
+105
View File
@@ -0,0 +1,105 @@
import { dumpSqlSelect, Select } from 'dbgate-sqltree';
import { EngineDriver } from 'dbgate-types';
import axios from '../utility/axios';
import _ from 'lodash';
import { extractDataColumns } from './DataChart';
export async function loadChartStructure(driver: EngineDriver, conid, database, sql) {
const select: Select = {
commandType: 'select',
selectAll: true,
topRecords: 1,
from: {
subQueryString: sql,
alias: 'subq',
},
};
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
if (resp.data.errorMessage) throw new Error(resp.data.errorMessage);
return resp.data.columns.map((x) => x.columnName);
}
export async function loadChartData(driver: EngineDriver, conid, database, sql, config) {
const dataColumns = extractDataColumns(config);
const { labelColumn, truncateFrom, truncateLimit, showRelativeValues } = config;
if (!labelColumn || !dataColumns || dataColumns.length == 0) return null;
const select: Select = {
commandType: 'select',
columns: [
{
exprType: 'column',
source: { alias: 'subq' },
columnName: labelColumn,
alias: labelColumn,
},
// @ts-ignore
...dataColumns.map((columnName) => ({
exprType: 'call',
func: 'SUM',
args: [
{
exprType: 'column',
columnName,
source: { alias: 'subq' },
},
],
alias: columnName,
})),
],
topRecords: truncateLimit || 100,
from: {
subQueryString: sql,
alias: 'subq',
},
groupBy: [
{
exprType: 'column',
source: { alias: 'subq' },
columnName: labelColumn,
},
],
orderBy: [
{
exprType: 'column',
source: { alias: 'subq' },
columnName: labelColumn,
direction: truncateFrom == 'end' ? 'DESC' : 'ASC',
},
],
};
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
let { rows, columns } = resp.data;
if (truncateFrom == 'end' && rows) {
rows = _.reverse([...rows]);
}
if (showRelativeValues) {
const maxValues = dataColumns.map((col) => _.max(rows.map((row) => row[col])));
for (const [col, max] of _.zip(dataColumns, maxValues)) {
if (!max) continue;
if (!_.isNumber(max)) continue;
if (!(max > 0)) continue;
rows = rows.map((row) => ({
...row,
[col]: (row[col] / max) * 100,
}));
// columns = columns.map((x) => {
// if (x.columnName == col) {
// return { columnName: `${col} %` };
// }
// return x;
// });
}
}
return {
columns,
rows,
};
}
@@ -5,7 +5,7 @@ import DropDownButton from '../widgets/DropDownButton';
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
import { useSplitterDrag } from '../widgets/Splitter';
import { isTypeDateTime } from 'dbgate-tools';
import { openDatabaseObjectDetail } from '../appobj/databaseObjectAppObject';
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject';
import { useSetOpenedTabs } from '../utility/globalState';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
+2 -2
View File
@@ -5,13 +5,13 @@ import styled from 'styled-components';
import { FontIcon } from '../icons';
const Label = styled.span`
font-weight: ${props => (props.notNull ? 'bold' : 'normal')};
font-weight: ${(props) => (props.notNull ? 'bold' : 'normal')};
white-space: nowrap;
`;
/** @param column {import('dbgate-datalib').DisplayColumn|import('dbgate-types').ColumnInfo} */
export default function ColumnLabel(column) {
let icon = null;
let icon = column.forceIcon ? 'img column' : null;
if (column.autoIncrement) icon = 'img autoincrement';
if (column.foreignKey) icon = 'img foreign-key';
return (
@@ -1,6 +1,6 @@
// @ts-nocheck
import React from 'react';
import { DropDownMenuItem, DropDownMenuDivider, showMenu } from '../modals/DropDownMenu';
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
import styled from 'styled-components';
import keycodes from '../utility/keycodes';
import { parseFilter, createMultiLineFilter } from 'dbgate-filterparser';
@@ -10,6 +10,7 @@ import FilterMultipleValuesModal from '../modals/FilterMultipleValuesModal';
import SetFilterModal from '../modals/SetFilterModal';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import { useShowMenu } from '../modals/showMenu';
// import { $ } from '../../Utility/jquery';
// import autobind from 'autobind-decorator';
// import * as React from 'react';
@@ -182,6 +183,7 @@ export default function DataFilterControl({
onFocusGrid,
}) {
const showModal = useShowModal();
const showMenu = useShowMenu();
const theme = useTheme();
const [filterState, setFilterState] = React.useState('empty');
const setFilterText = (filter) => {
@@ -12,6 +12,8 @@ export default function DataGridContextMenu({
filterSelectedValue,
openQuery,
openFreeTable,
openChartSelection,
openActiveChart,
}) {
return (
<>
@@ -53,6 +55,8 @@ export default function DataGridContextMenu({
)}
{openQuery && <DropDownMenuItem onClick={openQuery}>Open query</DropDownMenuItem>}
<DropDownMenuItem onClick={openFreeTable}>Open selection in free table editor</DropDownMenuItem>
<DropDownMenuItem onClick={openChartSelection}>Open chart from selection</DropDownMenuItem>
{openActiveChart && <DropDownMenuItem onClick={openActiveChart}>Open active chart</DropDownMenuItem>}
</>
);
}
+42 -16
View File
@@ -22,14 +22,14 @@ import DataGridToolbar from './DataGridToolbar';
// import usePropsCompare from '../utility/usePropsCompare';
import ColumnHeaderControl from './ColumnHeaderControl';
import InlineButton from '../widgets/InlineButton';
import { showMenu } from '../modals/DropDownMenu';
import DataGridContextMenu from './DataGridContextMenu';
import LoadingInfo from '../widgets/LoadingInfo';
import ErrorInfo from '../widgets/ErrorInfo';
import { openNewTab } from '../utility/common';
import { useSetOpenedTabs } from '../utility/globalState';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import { useShowMenu } from '../modals/showMenu';
import useOpenNewTab from '../utility/useOpenNewTab';
const GridContainer = styled.div`
position: absolute;
@@ -106,6 +106,7 @@ export default function DataGridCore(props) {
isLoadedAll,
loadedTime,
exportGrid,
openActiveChart,
allRowCount,
openQuery,
onSave,
@@ -117,7 +118,7 @@ export default function DataGridCore(props) {
} = props;
// console.log('RENDER GRID', display.baseTable.pureName);
const columns = React.useMemo(() => display.allColumns, [display]);
const setOpenedTabs = useSetOpenedTabs();
const openNewTab = useOpenNewTab();
// usePropsCompare(props);
@@ -138,6 +139,7 @@ export default function DataGridCore(props) {
const [autofillDragStartCell, setAutofillDragStartCell] = React.useState(nullCell);
const [autofillSelectedCells, setAutofillSelectedCells] = React.useState(emptyCellArray);
const [focusFilterInputs, setFocusFilterInputs] = React.useState({});
const showMenu = useShowMenu();
const autofillMarkerCell = React.useMemo(
() =>
@@ -320,22 +322,44 @@ export default function DataGridCore(props) {
setFirstVisibleColumnScrollIndex(value);
};
const handleOpenFreeTable = () => {
const getSelectedFreeData = () => {
const columns = getSelectedColumns();
const rows = getSelectedRowData().map((row) => _.pickBy(row, (v, col) => columns.find((x) => x.columnName == col)));
openNewTab(setOpenedTabs, {
title: 'selection',
icon: 'img free-table',
tabComponent: 'FreeTableTab',
props: {
initialData: {
structure: {
columns,
},
rows,
},
return {
structure: {
columns,
},
});
rows,
};
};
const handleOpenFreeTable = () => {
openNewTab(
{
title: 'selection',
icon: 'img free-table',
tabComponent: 'FreeTableTab',
props: {},
},
{ editor: getSelectedFreeData() }
);
};
const handleOpenChart = () => {
openNewTab(
{
title: 'Chart',
icon: 'img chart',
tabComponent: 'ChartTab',
props: {},
},
{
editor: {
data: getSelectedFreeData(),
config: { chartType: 'bar' },
},
}
);
};
const handleContextMenu = (event) => {
@@ -354,6 +378,8 @@ export default function DataGridCore(props) {
filterSelectedValue={display.filterable ? filterSelectedValue : null}
openQuery={openQuery}
openFreeTable={handleOpenFreeTable}
openChartSelection={handleOpenChart}
openActiveChart={openActiveChart}
/>
);
};
+25 -3
View File
@@ -6,13 +6,13 @@ import useSocket from '../utility/SocketProvider';
import useShowModal from '../modals/showModal';
import ImportExportModal from '../modals/ImportExportModal';
import { changeSetToSql, createChangeSet, getChangeSetInsertedRows } from 'dbgate-datalib';
import { openNewTab } from '../utility/common';
import LoadingDataGridCore from './LoadingDataGridCore';
import ChangeSetGrider from './ChangeSetGrider';
import { scriptToSql } from 'dbgate-sqltree';
import useModalState from '../modals/useModalState';
import ConfirmSqlModal from '../modals/ConfirmSqlModal';
import ErrorMessageModal from '../modals/ErrorMessageModal';
import useOpenNewTab from '../utility/useOpenNewTab';
/** @param props {import('./types').DataGridProps} */
async function loadDataPage(props, offset, limit) {
@@ -62,7 +62,7 @@ async function loadRowCount(props) {
export default function SqlDataGridCore(props) {
const { conid, database, display, changeSetState, dispatchChangeSet } = props;
const showModal = useShowModal();
const setOpenedTabs = useSetOpenedTabs();
const openNewTab = useOpenNewTab();
const confirmSqlModalState = useModalState();
const [confirmSql, setConfirmSql] = React.useState('');
@@ -80,8 +80,29 @@ export default function SqlDataGridCore(props) {
initialValues.sourceList = display.baseTable ? [display.baseTable.pureName] : [];
showModal((modalState) => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
}
function openActiveChart() {
openNewTab(
{
title: 'Chart',
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid,
database,
},
},
{
editor: {
config: { chartType: 'bar' },
sql: display.getExportQuery((select) => {
select.orderBy = null;
}),
},
}
);
}
function openQuery() {
openNewTab(setOpenedTabs, {
openNewTab({
title: 'Query',
icon: 'img sql-file',
tabComponent: 'QueryTab',
@@ -131,6 +152,7 @@ export default function SqlDataGridCore(props) {
<LoadingDataGridCore
{...props}
exportGrid={exportGrid}
openActiveChart={openActiveChart}
openQuery={openQuery}
loadDataPage={loadDataPage}
dataPageAvailable={dataPageAvailable}
+2 -2
View File
@@ -3,7 +3,7 @@ import ToolbarButton from '../widgets/ToolbarButton';
import styled from 'styled-components';
import { TabPage, TabControl } from '../widgets/TabControl';
import dimensions from '../theme/dimensions';
import JavaScriptEditor from '../sqleditor/JavaScriptEditor';
import GenericEditor from '../sqleditor/GenericEditor';
import MacroParameters from './MacroParameters';
import { WidgetTitle } from '../widgets/WidgetStyles';
import { FormButton } from '../utility/forms';
@@ -113,7 +113,7 @@ export default function MacroDetail({ selectedMacro, setSelectedMacro, onChangeV
</MacroDetailTabWrapper>
</TabPage>
<TabPage label="JavaScript" key="javascript">
<JavaScriptEditor readOnly value={selectedMacro.code} />
<GenericEditor readOnly value={selectedMacro.code} mode="javascript" />
</TabPage>
</TabControl>
</MacroDetailContainer>
+6 -3
View File
@@ -6,7 +6,7 @@ import SearchInput from '../widgets/SearchInput';
import { WidgetTitle } from '../widgets/WidgetStyles';
import macros from './macros';
import { AppObjectList } from '../appobj/AppObjectList';
import macroAppObject from '../appobj/MacroAppObject';
import MacroAppObject from '../appobj/MacroAppObject';
const SearchBoxWrapper = styled.div`
display: flex;
@@ -24,10 +24,13 @@ export default function MacroManager({ managerSize, selectedMacro, setSelectedMa
</SearchBoxWrapper>
<AppObjectList
list={_.sortBy(macros, 'title')}
makeAppObj={macroAppObject()}
AppObjectComponent={MacroAppObject}
onObjectClick={(macro) => setSelectedMacro(macro)}
getCommonProps={(data) => ({
isBold: selectedMacro && selectedMacro.name == data.name,
})}
filter={filter}
groupFunc={(appobj) => appobj.groupTitle}
groupFunc={(data) => data.group}
/>
{/* {macros.map((macro) => (
<MacroListItem key={`${macro.group}/${macro.name}`} macro={macro} />
@@ -1,8 +1,7 @@
import React from 'react';
import _ from 'lodash';
import { Formik, Form, useFormikContext } from 'formik';
import FormArgumentList from '../utility/FormArgumentList';
import { FormProvider } from '../utility/FormProvider';
export default function MacroParameters({ args, onChangeValues, macroValues, namePrefix }) {
if (!args || args.length == 0) return null;
@@ -11,10 +10,8 @@ export default function MacroParameters({ args, onChangeValues, macroValues, nam
...macroValues,
};
return (
<Formik initialValues={initialValues} onSubmit={() => {}}>
<Form>
<FormArgumentList args={args} onChangeValues={onChangeValues} namePrefix={namePrefix} />
</Form>
</Formik>
<FormProvider initialValues={initialValues}>
<FormArgumentList args={args} onChangeValues={onChangeValues} namePrefix={namePrefix} />
</FormProvider>
);
}
@@ -1,12 +1,11 @@
import _ from 'lodash';
import { useSetOpenedTabs } from '../utility/globalState';
import { openNewTab } from '../utility/common';
import useOpenNewTab from '../utility/useOpenNewTab';
export default function useNewFreeTable() {
const setOpenedTabs = useSetOpenedTabs();
const openNewTab = useOpenNewTab();
return ({ title = undefined, ...props } = {}) =>
openNewTab(setOpenedTabs, {
openNewTab({
title: title || 'Table',
icon: 'img free-table',
tabComponent: 'FreeTableTab',
+12 -2
View File
@@ -1,5 +1,4 @@
import React from 'react';
import _ from 'lodash';
const iconNames = {
'icon minus-box': 'mdi mdi-minus-box-outline',
@@ -10,6 +9,8 @@ const iconNames = {
'icon export': 'mdi mdi-application-export',
'icon new-connection': 'mdi mdi-database-plus',
'icon tables': 'mdi mdi-table-multiple',
'icon favorite': 'mdi mdi-star',
'icon share': 'mdi mdi-share-variant',
'icon database': 'mdi mdi-database',
'icon server': 'mdi mdi-server',
@@ -26,6 +27,8 @@ const iconNames = {
'icon save': 'mdi mdi-content-save',
'icon account': 'mdi mdi-account',
'icon sql-file': 'mdi mdi-file',
'icon web': 'mdi mdi-web',
'icon home': 'mdi mdi-home',
'icon edit': 'mdi mdi-pencil',
'icon delete': 'mdi mdi-delete',
@@ -39,14 +42,17 @@ const iconNames = {
'icon theme': 'mdi mdi-brightness-6',
'icon error': 'mdi mdi-close-circle',
'icon ok': 'mdi mdi-check-circle',
'icon markdown': 'mdi mdi-application',
'icon preview': 'mdi mdi-file-find',
'icon run': 'mdi mdi-play',
'icon chevron-down': 'mdi mdi-chevron-down',
'icon plugin': 'mdi mdi-toy-brick',
'img green-ok': 'mdi mdi-check-circle color-green-8',
'img ok': 'mdi mdi-check-circle color-green-8',
'img alert': 'mdi mdi-alert-circle color-blue-6',
'img error': 'mdi mdi-close-circle color-red-7',
'img warn': 'mdi mdi-alert color-gold-7',
// 'img statusbar-ok': 'mdi mdi-check-circle color-on-statusbar-green',
'img archive': 'mdi mdi-table color-gold-7',
@@ -58,6 +64,10 @@ const iconNames = {
'img foreign-key': 'mdi mdi-key-link',
'img sql-file': 'mdi mdi-file',
'img shell': 'mdi mdi-flash color-blue-7',
'img chart': 'mdi mdi-chart-bar color-magenta-7',
'img markdown': 'mdi mdi-application color-red-7',
'img preview': 'mdi mdi-file-find color-red-7',
'img favorite': 'mdi mdi-star color-yellow-7',
'img free-table': 'mdi mdi-table color-green-7',
'img macro': 'mdi mdi-hammer-wrench',
@@ -1,7 +1,6 @@
import React from 'react';
import _ from 'lodash';
import FormStyledButton from '../widgets/FormStyledButton';
import { useFormikContext } from 'formik';
import styled from 'styled-components';
import {
FormReactSelect,
@@ -12,14 +11,13 @@ import {
FormArchiveFolderSelect,
FormArchiveFilesSelect,
} from '../utility/forms';
import { useArchiveFiles, useConnectionInfo, useDatabaseInfo, useInstalledPlugins } from '../utility/metadataLoaders';
import { useArchiveFiles, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import TableControl, { TableColumn } from '../utility/TableControl';
import { TextField, SelectField, CheckboxField } from '../utility/inputs';
import { createPreviewReader, getActionOptions, getTargetName } from './createImpExpScript';
import getElectron from '../utility/getElectron';
import ErrorInfo from '../widgets/ErrorInfo';
import getAsArray from '../utility/getAsArray';
import axios from '../utility/axios';
import LoadingInfo from '../widgets/LoadingInfo';
import SqlEditor from '../sqleditor/SqlEditor';
import { useUploadsProvider } from '../utility/UploadsProvider';
@@ -28,6 +26,10 @@ import useTheme from '../theme/useTheme';
import { findFileFormat, getFileFormatDirections } from '../utility/fileformats';
import FormArgumentList from '../utility/FormArgumentList';
import useExtensions from '../utility/useExtensions';
import UploadButton from '../utility/UploadButton';
import useShowModal from '../modals/showModal';
import ChangeDownloadUrlModal from '../modals/ChangeDownloadUrlModal';
import { useForm } from '../utility/FormProvider';
const Container = styled.div`
// max-height: 50vh;
@@ -59,12 +61,17 @@ const SourceNameWrapper = styled.div`
justify-content: space-between;
`;
const TrashWrapper = styled.div`
const SourceNameButtons = styled.div`
display: flex;
`;
const IconButtonWrapper = styled.div`
&:hover {
background-color: ${(props) => props.theme.modal_background2};
}
cursor: pointer;
color: ${(props) => props.theme.modal_font_blue[7]};
margin-left: 5px;
`;
const SqlWrapper = styled.div`
@@ -90,6 +97,10 @@ const Title = styled.div`
margin: 10px 0px;
`;
const ButtonsLine = styled.div`
display: flex;
`;
function getFileFilters(extensions, storageType) {
const res = [];
const format = findFileFormat(extensions, storageType);
@@ -98,11 +109,12 @@ function getFileFilters(extensions, storageType) {
return res;
}
async function addFilesToSourceListDefault(file, newSources, newValues) {
const sourceName = file.name;
async function addFileToSourceListDefault({ fileName, shortName, isDownload }, newSources, newValues) {
const sourceName = shortName;
newSources.push(sourceName);
newValues[`sourceFile_${sourceName}`] = {
fileName: file.full,
fileName,
isDownload,
};
}
@@ -113,7 +125,7 @@ async function addFilesToSourceList(extensions, files, values, setValues, prefer
for (const file of getAsArray(files)) {
const format = findFileFormat(extensions, storage);
if (format) {
await (format.addFilesToSourceList || addFilesToSourceListDefault)(file, newSources, newValues);
await (format.addFileToSourceList || addFileToSourceListDefault)(file, newSources, newValues);
}
}
newValues['sourceList'] = [...(values.sourceList || []).filter((x) => !newSources.includes(x)), ...newSources];
@@ -130,7 +142,7 @@ async function addFilesToSourceList(extensions, files, values, setValues, prefer
}
function ElectronFilesInput() {
const { values, setValues } = useFormikContext();
const { values, setValues } = useForm();
const electron = getElectron();
const [isLoading, setIsLoading] = React.useState(false);
const extensions = useExtensions();
@@ -147,8 +159,8 @@ function ElectronFilesInput() {
await addFilesToSourceList(
extensions,
files.map((full) => ({
full,
...path.parse(full),
fileName: full,
shortName: path.parse(full).name,
})),
values,
setValues
@@ -167,13 +179,51 @@ function ElectronFilesInput() {
);
}
function FilesInput() {
function extractUrlName(url, values) {
const match = url.match(/\/([^/]+)($|\?)/);
if (match) {
const res = match[1];
if (res.includes('.')) {
return res.slice(0, res.indexOf('.'));
}
return res;
}
return `url${values && values.sourceList ? values.sourceList.length + 1 : '1'}`;
}
function FilesInput({ setPreviewSource = undefined }) {
const theme = useTheme();
const electron = getElectron();
if (electron) {
return <ElectronFilesInput />;
}
return <DragWrapper theme={theme}>Drag &amp; drop imported files here</DragWrapper>;
const showModal = useShowModal();
const { values, setValues } = useForm();
const extensions = useExtensions();
const doAddUrl = (url) => {
addFilesToSourceList(
extensions,
[
{
fileName: url,
shortName: extractUrlName(url, values),
isDownload: true,
},
],
values,
setValues,
null,
setPreviewSource
);
};
const handleAddUrl = () =>
showModal((modalState) => <ChangeDownloadUrlModal modalState={modalState} onConfirm={doAddUrl} />);
return (
<>
<ButtonsLine>
{electron ? <ElectronFilesInput /> : <UploadButton />}
<FormStyledButton value="Add web URL" onClick={handleAddUrl} />
</ButtonsLine>
<DragWrapper theme={theme}>Drag &amp; drop imported files here</DragWrapper>
</>
);
}
function SourceTargetConfig({
@@ -185,10 +235,11 @@ function SourceTargetConfig({
schemaNameField,
tablesField = undefined,
engine = undefined,
setPreviewSource = undefined,
}) {
const extensions = useExtensions();
const theme = useTheme();
const { values, setFieldValue } = useFormikContext();
const { values, setFieldValue } = useForm();
const types =
values[storageTypeField] == 'jsldata'
? [{ value: 'jsldata', label: 'Query result data', directions: ['source'] }]
@@ -311,7 +362,7 @@ function SourceTargetConfig({
</>
)}
{!!format && direction == 'source' && <FilesInput />}
{!!format && direction == 'source' && <FilesInput setPreviewSource={setPreviewSource} />}
{format && format.args && (
<FormArgumentList
@@ -324,27 +375,44 @@ function SourceTargetConfig({
}
function SourceName({ name }) {
const { values, setFieldValue } = useFormikContext();
const { values, setFieldValue } = useForm();
const theme = useTheme();
const showModal = useShowModal();
const obj = values[`sourceFile_${name}`];
const handleDelete = () => {
setFieldValue(
'sourceList',
values.sourceList.filter((x) => x != name)
);
};
const doChangeUrl = (url) => {
setFieldValue(`sourceFile_${name}`, { fileName: url, isDownload: true });
};
const handleChangeUrl = () => {
showModal((modalState) => (
<ChangeDownloadUrlModal modalState={modalState} url={obj.fileName} onConfirm={doChangeUrl} />
));
};
return (
<SourceNameWrapper>
<div>{name}</div>
<TrashWrapper onClick={handleDelete} theme={theme}>
<FontIcon icon="icon delete" />
</TrashWrapper>
<SourceNameButtons>
{obj && !!obj.isDownload && (
<IconButtonWrapper onClick={handleChangeUrl} theme={theme} title={obj && obj.fileName}>
<FontIcon icon="icon web" />
</IconButtonWrapper>
)}
<IconButtonWrapper onClick={handleDelete} theme={theme}>
<FontIcon icon="icon delete" />
</IconButtonWrapper>
</SourceNameButtons>
</SourceNameWrapper>
);
}
export default function ImportExportConfigurator({ uploadedFile = undefined, onChangePreview = undefined }) {
const { values, setFieldValue, setValues } = useFormikContext();
const { values, setFieldValue, setValues } = useForm();
const targetDbinfo = useDatabaseInfo({ conid: values.targetConnectionId, database: values.targetDatabaseName });
const sourceConnectionInfo = useConnectionInfo({ conid: values.sourceConnectionId });
const { engine: sourceEngine } = sourceConnectionInfo || {};
@@ -356,13 +424,12 @@ export default function ImportExportConfigurator({ uploadedFile = undefined, onC
const handleUpload = React.useCallback(
(file) => {
console.log('UPLOAD', extensions);
addFilesToSourceList(
extensions,
[
{
full: file.filePath,
name: file.shortName,
fileName: file.filePath,
shortName: file.shortName,
},
],
values,
@@ -389,6 +456,8 @@ export default function ImportExportConfigurator({ uploadedFile = undefined, onC
}, []);
const supportsPreview = !!findFileFormat(extensions, values.sourceStorageType);
const previewFileName =
previewSource && values[`sourceFile_${previewSource}`] && values[`sourceFile_${previewSource}`].fileName;
const handleChangePreviewSource = async () => {
if (previewSource && supportsPreview) {
@@ -401,7 +470,7 @@ export default function ImportExportConfigurator({ uploadedFile = undefined, onC
React.useEffect(() => {
handleChangePreviewSource();
}, [previewSource, supportsPreview]);
}, [previewSource, supportsPreview, previewFileName]);
const oldValues = React.useRef({});
React.useEffect(() => {
@@ -427,6 +496,7 @@ export default function ImportExportConfigurator({ uploadedFile = undefined, onC
schemaNameField="sourceSchemaName"
tablesField="sourceList"
engine={sourceEngine}
setPreviewSource={setPreviewSource}
/>
<ArrowWrapper theme={theme}>
<FontIcon icon="icon arrow-right" />
+14 -12
View File
@@ -2,11 +2,11 @@ import _ from 'lodash';
import { extractShellApiFunctionName, extractShellApiPlugins } from 'dbgate-tools';
export default class ScriptWriter {
constructor() {
constructor(varCount = '0') {
this.s = '';
this.packageNames = [];
// this.engines = [];
this.varCount = 0;
this.varCount = parseInt(varCount) || 0;
}
allocVariable(prefix = 'var') {
@@ -24,6 +24,10 @@ export default class ScriptWriter {
this.packageNames.push(...extractShellApiPlugins(functionName, props));
}
requirePackage(packageName) {
this.packageNames.push(packageName);
}
copyStream(sourceVar, targetVar) {
this.put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`);
}
@@ -32,16 +36,14 @@ export default class ScriptWriter {
this.put(`// ${s}`);
}
getScript(extensions) {
// if (this.packageNames.length > 0) {
// this.comment('@packages');
// this.comment(JSON.stringify(this.packageNames));
// }
// if (this.engines.length > 0) {
// this.comment('@engines');
// this.comment(JSON.stringify(this.engines));
// }
getScript(schedule = null) {
const packageNames = this.packageNames;
return _.uniq(packageNames).map((packageName) => `// @require ${packageName}\n`).join('') + '\n' + this.s;
let prefix = _.uniq(packageNames)
.map((packageName) => `// @require ${packageName}\n`)
.join('');
if (schedule) prefix += `// @schedule ${schedule}`;
if (prefix) prefix += '\n';
return prefix + this.s;
}
}
@@ -68,7 +68,7 @@ function getSourceExpr(extensions, sourceName, values, sourceConnection, sourceD
return [
format.readerFunc,
{
...sourceFile,
..._.omit(sourceFile, ['isDownload']),
...extractApiParameters(values, 'source', format),
},
];
@@ -150,7 +150,7 @@ function getTargetExpr(extensions, sourceName, values, targetConnection, targetD
}
export default async function createImpExpScript(extensions, values, addEditorInfo = true) {
const script = new ScriptWriter();
const script = new ScriptWriter(values.startVariableIndex || 0);
const [sourceConnection, sourceDriver] = await getConnection(
extensions,
@@ -182,7 +182,7 @@ export default async function createImpExpScript(extensions, values, addEditorIn
script.comment('@ImportExportConfigurator');
script.comment(JSON.stringify(values));
}
return script.getScript(extensions);
return script.getScript(values.schedule);
}
export function getActionOptions(extensions, source, values, targetDbinfo) {
+2
View File
@@ -9,7 +9,9 @@ import 'ace-builds/src-noconflict/mode-sql';
import 'ace-builds/src-noconflict/mode-mysql';
import 'ace-builds/src-noconflict/mode-pgsql';
import 'ace-builds/src-noconflict/mode-sqlserver';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/mode-javascript';
import 'ace-builds/src-noconflict/mode-markdown';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-twilight';
import 'ace-builds/src-noconflict/ext-searchbox';
@@ -0,0 +1,34 @@
import React from 'react';
import Markdown from 'markdown-to-jsx';
import styled from 'styled-components';
import OpenChartLink from './OpenChartLink';
import MarkdownLink from './MarkdownLink';
import OpenSqlLink from './OpenSqlLink';
const Wrapper = styled.div`
padding: 10px;
overflow: auto;
flex: 1;
`;
export default function MarkdownExtendedView({ children }) {
return (
<Wrapper>
<Markdown
options={{
overrides: {
OpenChartLink: {
component: OpenChartLink,
},
OpenSqlLink: {
component: OpenSqlLink,
},
a: MarkdownLink,
},
}}
>
{children || ''}
</Markdown>
</Wrapper>
);
}
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
export default function MarkdownLink({ href, title, children }) {
const theme = useTheme();
return (
<StyledThemedLink theme={theme} href={href} title={title} target="_blank">
{children}
</StyledThemedLink>
);
}
@@ -0,0 +1,20 @@
import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton';
export default function MarkdownToolbar({ save, showPreview }) {
const hasPermission = useHasPermission();
return (
<>
{hasPermission('files/markdown/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton onClick={showPreview} icon="icon preview">
Preview
</ToolbarButton>
</>
);
}
@@ -0,0 +1,35 @@
import React from 'react';
import { useCurrentDatabase } from '../utility/globalState';
import axios from '../utility/axios';
import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
import useOpenNewTab from '../utility/useOpenNewTab';
export default function OpenChartLink({ file, children }) {
const openNewTab = useOpenNewTab();
const currentDb = useCurrentDatabase();
const theme = useTheme();
const handleClick = async () => {
const resp = await axios.post('files/load', { folder: 'charts', file, format: 'json' });
openNewTab(
{
title: file,
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid: currentDb && currentDb.connection && currentDb.connection._id,
database: currentDb && currentDb.name,
savedFile: file,
},
},
{ editor: resp.data }
);
};
return (
<StyledThemedLink theme={theme} onClick={handleClick}>
{children}
</StyledThemedLink>
);
}
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import axios from '../utility/axios';
import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
import useNewQuery from '../query/useNewQuery';
export default function OpenSqlLink({ file, children }) {
const newQuery = useNewQuery();
const theme = useTheme();
const handleClick = async () => {
const resp = await axios.post('files/load', { folder: 'sql', file, format: 'text' });
newQuery({
title: file,
initialData: resp.data,
// @ts-ignore
savedFile: file,
});
};
return (
<StyledThemedLink theme={theme} onClick={handleClick}>
{children}
</StyledThemedLink>
);
}
+90
View File
@@ -0,0 +1,90 @@
import React from 'react';
import ModalBase from './ModalBase';
import ModalHeader from './ModalHeader';
import ModalContent from './ModalContent';
import ModalFooter from './ModalFooter';
import { useConfig } from '../utility/metadataLoaders';
import FormStyledButton from '../widgets/FormStyledButton';
import moment from 'moment';
import styled from 'styled-components';
import getElectron from '../utility/getElectron';
import useTheme from '../theme/useTheme';
import { StyledThemedLink } from '../widgets/FormStyledButton';
const Container = styled.div`
display: flex;
`;
const TextContainer = styled.div``;
const StyledLine = styled.div`
margin: 5px;
`;
const StyledValue = styled.span`
font-weight: bold;
`;
function Line({ label, children }) {
return (
<StyledLine>
{label}: <StyledValue>{children}</StyledValue>
</StyledLine>
);
}
function Link({ label, children, href }) {
const electron = getElectron();
const theme = useTheme();
return (
<StyledLine>
{label}:{' '}
{electron ? (
<StyledThemedLink theme={theme} onClick={() => electron.shell.openExternal(href)}>
{children}
</StyledThemedLink>
) : (
<StyledThemedLink theme={theme} href={href} target="_blank" rel="noopener noreferrer">
{children}
</StyledThemedLink>
)}
</StyledLine>
);
}
export default function AboutModal({ modalState }) {
const config = useConfig();
const { version, buildTime } = config || {};
return (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>About DbGate</ModalHeader>
<ModalContent>
<Container>
<img src="/logo192.png" />
<TextContainer>
<Line label="Version">{version}</Line>
<Line label="Build date">{moment(buildTime).format('YYYY-MM-DD')}</Line>
<Link label="Web" href="https://dbgate.org">
dbgate.org
</Link>
<Link label="Source codes" href="https://github.com/dbshell/dbgate/">
github
</Link>
<Link label="Docker container" href="https://hub.docker.com/r/dbgate/dbgate">
docker hub
</Link>
<Link label="Online demo" href="https://demo.dbgate.org">
demo.dbgate.org
</Link>
<Link label="Search plugins" href="https://www.npmjs.com/search?q=keywords:dbgateplugin">
npmjs.com
</Link>
</TextContainer>
</Container>
</ModalContent>
<ModalFooter>
<FormStyledButton value="Close" onClick={() => modalState.close()} />
</ModalFooter>
</ModalBase>
);
}
@@ -0,0 +1,42 @@
import React from 'react';
import ModalBase from './ModalBase';
import { FormButton, FormSubmit, FormTextField } from '../utility/forms';
import ModalHeader from './ModalHeader';
import ModalContent from './ModalContent';
import ModalFooter from './ModalFooter';
import FormStyledButton from '../widgets/FormStyledButton';
import { FormProvider } from '../utility/FormProvider';
export default function ChangeDownloadUrlModal({ modalState, url = '', onConfirm = undefined }) {
// const textFieldRef = React.useRef(null);
// React.useEffect(() => {
// if (textFieldRef.current) textFieldRef.current.focus();
// }, [textFieldRef.current]);
// const handleSubmit = () => async (values) => {
// onConfirm(values.url);
// modalState.close();
// };
const handleSubmit = React.useCallback(
async (values) => {
onConfirm(values.url);
modalState.close();
},
[modalState, onConfirm]
);
return (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>Download imported file from web</ModalHeader>
<FormProvider initialValues={{ url }}>
<ModalContent>
<FormTextField label="URL" name="url" style={{ width: '30vw' }} focused />
</ModalContent>
<ModalFooter>
<FormSubmit value="OK" onClick={handleSubmit} />
<FormStyledButton value="Cancel" onClick={() => modalState.close()} />
</ModalFooter>
</FormProvider>
</ModalBase>
);
}
+17 -13
View File
@@ -3,22 +3,26 @@ import ModalBase from './ModalBase';
import FormStyledButton from '../widgets/FormStyledButton';
import ModalFooter from './ModalFooter';
import ModalContent from './ModalContent';
import { FormSubmit } from '../utility/forms';
import { FormProvider } from '../utility/FormProvider';
export default function ConfirmModal({ message, modalState, onConfirm }) {
return (
<ModalBase modalState={modalState}>
<ModalContent>{message}</ModalContent>
<FormProvider>
<ModalBase modalState={modalState}>
<ModalContent>{message}</ModalContent>
<ModalFooter>
<FormStyledButton
value="OK"
onClick={() => {
modalState.close();
onConfirm();
}}
/>
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
</ModalFooter>
</ModalBase>
<ModalFooter>
<FormSubmit
value="OK"
onClick={() => {
modalState.close();
onConfirm();
}}
/>
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
</ModalFooter>
</ModalBase>
</FormProvider>
);
}
@@ -1,6 +1,5 @@
import React from 'react';
import ModalBase from './ModalBase';
import { FormButtonRow } from '../utility/forms';
import FormStyledButton from '../widgets/FormStyledButton';
import SqlEditor from '../sqleditor/SqlEditor';
import styled from 'styled-components';
+57 -33
View File
@@ -1,62 +1,86 @@
import React from 'react';
import axios from '../utility/axios';
import ModalBase from './ModalBase';
import { FormButtonRow, FormButton, FormTextField, FormSelectField, FormSubmit } from '../utility/forms';
import { TextField } from '../utility/inputs';
import { Formik, Form } from 'formik';
import { FormButton, FormTextField, FormSelectField, FormSubmit } from '../utility/forms';
import ModalHeader from './ModalHeader';
import ModalFooter from './ModalFooter';
import ModalContent from './ModalContent';
import useExtensions from '../utility/useExtensions';
import LoadingInfo from '../widgets/LoadingInfo';
import { FontIcon } from '../icons';
import { FormProvider } from '../utility/FormProvider';
// import FormikForm from '../utility/FormikForm';
export default function ConnectionModal({ modalState, connection = undefined }) {
const [sqlConnectResult, setSqlConnectResult] = React.useState('Not connected');
const [sqlConnectResult, setSqlConnectResult] = React.useState(null);
const extensions = useExtensions();
const [isTesting, setIsTesting] = React.useState(false);
const testIdRef = React.useRef(0);
const handleTest = async (values) => {
setIsTesting(true);
testIdRef.current += 1;
const testid = testIdRef.current;
const resp = await axios.post('connections/test', values);
const { error, version } = resp.data;
if (testIdRef.current != testid) return;
setSqlConnectResult(error || version);
setIsTesting(false);
setSqlConnectResult(resp.data);
};
const handleCancel = async () => {
testIdRef.current += 1; // invalidate current test
setIsTesting(false);
};
const handleSubmit = async (values) => {
const resp = await axios.post('connections/save', values);
axios.post('connections/save', values);
modalState.close();
};
return (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>{connection ? 'Edit connection' : 'Add connection'}</ModalHeader>
<Formik onSubmit={handleSubmit} initialValues={connection || { server: 'localhost', engine: 'mssql' }}>
<Form>
<ModalContent>
<FormSelectField label="Database engine" name="engine">
<option value=""></option>
{extensions.drivers.map((driver) => (
<option value={driver.engine} key={driver.engine}>
{driver.title}
</option>
))}
{/* <option value="mssql">Microsoft SQL Server</option>
<FormProvider initialValues={connection || { server: 'localhost', engine: 'mssql' }}>
<ModalContent>
<FormSelectField label="Database engine" name="engine">
<option value=""></option>
{extensions.drivers.map((driver) => (
<option value={driver.engine} key={driver.engine}>
{driver.title}
</option>
))}
{/* <option value="mssql">Microsoft SQL Server</option>
<option value="mysql">MySQL</option>
<option value="postgres">Postgre SQL</option> */}
</FormSelectField>
<FormTextField label="Server" name="server" />
<FormTextField label="Port" name="port" />
<FormTextField label="User" name="user" />
<FormTextField label="Password" name="password" />
<FormTextField label="Display name" name="displayName" />
<div>Connect result: {sqlConnectResult}</div>
</ModalContent>
</FormSelectField>
<FormTextField label="Server" name="server" />
<FormTextField label="Port" name="port" />
<FormTextField label="User" name="user" />
<FormTextField label="Password" name="password" type="password" />
<FormTextField label="Display name" name="displayName" />
{!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'connected' && (
<div>
Connected: <FontIcon icon="img ok" /> {sqlConnectResult.version}
</div>
)}
{!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'error' && (
<div>
Connect failed: <FontIcon icon="img error" /> {sqlConnectResult.error}
</div>
)}
{isTesting && <LoadingInfo message="Testing connection" />}
</ModalContent>
<ModalFooter>
<FormButton text="Test" onClick={handleTest} />
<FormSubmit text="Save" />
</ModalFooter>
</Form>
</Formik>
<ModalFooter>
{isTesting ? (
<FormButton value="Cancel" onClick={handleCancel} />
) : (
<FormButton value="Test" onClick={handleTest} />
)}
<FormSubmit value="Save" onClick={handleSubmit} />
</ModalFooter>
</FormProvider>
</ModalBase>
);
}
+10 -12
View File
@@ -1,11 +1,11 @@
import React from 'react';
import axios from '../utility/axios';
import ModalBase from './ModalBase';
import { FormTextField, FormSubmit } from '../utility/forms';
import { Formik, Form } from 'formik';
import { FormButton, FormSubmit, FormTextField } from '../utility/forms';
import ModalHeader from './ModalHeader';
import ModalContent from './ModalContent';
import ModalFooter from './ModalFooter';
import { FormProvider } from '../utility/FormProvider';
export default function CreateDatabaseModal({ modalState, conid }) {
const handleSubmit = async (values) => {
@@ -17,16 +17,14 @@ export default function CreateDatabaseModal({ modalState, conid }) {
return (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>Create database</ModalHeader>
<Formik onSubmit={handleSubmit} initialValues={{ name: 'newdb' }}>
<Form>
<ModalContent>
<FormTextField label="Database name" name="name" />
</ModalContent>
<ModalFooter>
<FormSubmit text="Create" />
</ModalFooter>
</Form>
</Formik>
<FormProvider initialValues={{ name: 'newdb' }}>
<ModalContent>
<FormTextField label="Database name" name="name" />
</ModalContent>
<ModalFooter>
<FormSubmit value="Create" onClick={handleSubmit} />
</ModalFooter>
</FormProvider>
</ModalBase>
);
}
+17 -209
View File
@@ -1,7 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components';
import { LoadingToken, sleep } from '../utility/common';
import { sleep } from '../utility/common';
import useDocumentClick from '../utility/useDocumentClick';
import { useHideMenu } from './showMenu';
const ContextMenuStyled = styled.ul`
position: absolute;
@@ -60,214 +61,21 @@ export function DropDownMenuItem({ children, keyText = undefined, onClick }) {
);
}
// (DropDownMenuItem as any).contextTypes = {
// parentMenu: PropTypes.any
// };
// interface IDropDownMenuLinkProps {
// href: string;
// keyText?: string;
// }
// export class DropDownMenuLink extends React.Component<IDropDownMenuLinkProps> {
// render() {
// return <li onMouseEnter={this.handleMouseEnter.bind(this)}><Link forceSimpleLink href={this.props.href}>{this.props.children}{this.props.keyText && <span className='context_menu_key_text'>{this.props.keyText}</span>}</Link></li>;
// }
// handleMouseEnter() {
// if (this.context.parentMenu) this.context.parentMenu.closeSubmenu();
// }
// }
// (DropDownMenuLink as any).contextTypes = {
// parentMenu: PropTypes.any
// };
// // export function DropDownMenu(props: { children?: any }) {
// // return <div className="btn-group">
// // <button type="button" className="btn btn-default dropdown-toggle btn-xs" data-toggle="dropdown"
// // aria-haspopup="true" aria-expanded="false" tabIndex={-1}>
// // <span className="caret"></span>
// // </button>
// // <ul className="dropdown-menu">
// // {props.children}
// // </ul>
// // </div>
// // }
// export function DropDownMenuDivider(props: {}) {
// return <li className="dropdown-divider"></li>;
// }
// export class DropDownSubmenuItem extends React.Component<IDropDownSubmenuItemProps> {
// menuInstance: ContextMenu;
// domObject: Element;
// render() {
// return <li onMouseEnter={this.handleMouseEnter.bind(this)} ref={x => this.domObject = x}><Link onClick={() => null}>{this.props.title} <IconSpan icon='fa-caret-right' /></Link></li>;
// }
// closeSubmenu() {
// if (this.menuInstance != null) {
// this.menuInstance.close();
// this.menuInstance = null;
// }
// if (this.context.parentMenu) this.context.parentMenu.submenu = null;
// }
// closeOtherSubmenu() {
// if (this.context.parentMenu) this.context.parentMenu.closeSubmenu();
// }
// handleMouseEnter() {
// this.closeOtherSubmenu();
// let offset = $(this.domObject).offset();
// let width = $(this.domObject).width();
// this.menuInstance = showMenuCore(offset.left + width, offset.top, this);
// if (this.context.parentMenu) this.context.parentMenu.submenu = this;
// }
// }
// (DropDownSubmenuItem as any).contextTypes = {
// parentMenu: PropTypes.any
// };
// export class DropDownMenu extends React.Component<IDropDownMenuProps, IDropDownMenuState> {
// domButton: Element;
// constructor(props) {
// super(props);
// this.state = {
// isExpanded: false,
// };
// }
// render() {
// let className = this.props.classOverride || ('btn btn-xs btn-default drop_down_menu_button ' + (this.props.className || ''));
// return <button id={this.props.buttonElementId} type="button" className={className} tabIndex={-1} onClick={this.menuButtonClick} ref={x => this.domButton = x}>
// { this.props.title }
// { this.props.iconSpan || <span className="caret"></span>}
// </button>
// }
// @autobind
// menuButtonClick() {
// if (this.state.isExpanded) {
// hideMenu();
// return;
// }
// let offset = $(this.domButton).offset();
// let height = $(this.domButton).height();
// this.setState({ isExpanded: true })
// showMenu(offset.left, offset.top + height + 5, this, () => this.setState({ isExpanded: false }));
// }
// }
export function ContextMenu({ left, top, children }) {
return <ContextMenuStyled style={{ left: `${left}px`, top: `${top}px` }}>{children}</ContextMenuStyled>;
}
// export class ContextMenu extends React.Component<IContextMenuProps> {
// domObject: Element;
// submenu: DropDownSubmenuItem;
// render() {
// return <ul className='context_menu' style={{ left: `${this.props.left}px`, top: `${this.props.top}px` }} ref={x => this.domObject = x} onContextMenu={e => e.preventDefault()}>
// {this.props.children}
// </ul>;
// }
// componentDidMount() {
// fixPopupPlacement(this.domObject);
// }
// getChildContext() {
// return { parentMenu: this };
// }
// closeSubmenu() {
// if (this.submenu) {
// this.submenu.closeSubmenu();
// }
// }
// close() {
// this.props.container.remove();
// this.closeSubmenu();
// }
// }
// (ContextMenu as any).childContextTypes = {
// parentMenu: PropTypes.any
// };
let menuHandle = null;
let hideToken = null;
function showMenuCore(left, top, contentHolder, closeCallback = null) {
let container = document.createElement('div');
let handle = {
container,
closeCallback,
close() {
this.container.remove();
},
};
document.body.appendChild(container);
ReactDOM.render(
<ContextMenu left={left} top={top}>
{contentHolder}
</ContextMenu>,
container
const hideMenu = useHideMenu();
useDocumentClick(async () => {
await sleep(0);
hideMenu();
});
const menuRef = React.useRef(null);
React.useEffect(() => {
if (menuRef.current) fixPopupPlacement(menuRef.current);
}, [menuRef.current]);
return (
<ContextMenuStyled ref={menuRef} style={{ left: `${left}px`, top: `${top}px` }}>
{children}
</ContextMenuStyled>
);
return handle;
}
export function showMenu(left, top, contentHolder, closeCallback = null) {
hideMenu();
if (hideToken) hideToken.cancel();
menuHandle = showMenuCore(left, top, contentHolder, closeCallback);
captureMouseDownEvents();
}
function captureMouseDownEvents() {
document.addEventListener('mousedown', mouseDownListener, true);
}
function releaseMouseDownEvents() {
document.removeEventListener('mousedown', mouseDownListener, true);
}
function captureMouseUpEvents() {
document.addEventListener('mouseup', mouseUpListener, true);
}
function releaseMouseUpEvents() {
document.removeEventListener('mouseup', mouseUpListener, true);
}
async function mouseDownListener(e) {
captureMouseUpEvents();
}
async function mouseUpListener(e) {
let token = new LoadingToken();
hideToken = token;
await sleep(0);
if (token.isCanceled) return;
hideMenu();
}
function hideMenu() {
if (menuHandle == null) return;
menuHandle.close();
if (menuHandle.closeCallback) menuHandle.closeCallback();
menuHandle = null;
releaseMouseDownEvents();
releaseMouseUpEvents();
}
function getElementOffset(element) {
@@ -278,7 +86,7 @@ function getElementOffset(element) {
return { top: top, left: left };
}
export function fixPopupPlacement(element) {
function fixPopupPlacement(element) {
const { width, height } = element.getBoundingClientRect();
let offset = getElementOffset(element);

Some files were not shown because too many files have changed in this diff Show More