Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f5d866905 | |||
| ed15db4c20 | |||
| a7926a1a71 | |||
| 3a89d1a07b | |||
| 808267d97a | |||
| 4ab59e09d7 | |||
| c108c7d787 | |||
| fa5e128d45 | |||
| 1a181ff714 | |||
| 0f16f842bf | |||
| e502f5e72c | |||
| 2978063ac8 | |||
| ae0606cc84 | |||
| 0c0c0356a6 | |||
| 1e447a8937 | |||
| 3dcc761c14 | |||
| b1a2093e6b | |||
| ed98a9e2da | |||
| 737298c6f3 | |||
| 0081deb844 | |||
| 0857757ce2 | |||
| f7bab744e6 | |||
| 0316cb16eb | |||
| adcab4ae69 | |||
| 728353d60a | |||
| ac4aa94976 | |||
| d502dc0dfd | |||
| 55d0c77536 | |||
| 79ddbd439f | |||
| 7a0883ea03 | |||
| 5256deb567 | |||
| f993e82b0b | |||
| 698756b9d2 | |||
| 3921913742 | |||
| 576bdd64a0 | |||
| 27a78facf5 | |||
| ee50604112 | |||
| 1c30eb337b | |||
| 6a78272cc5 | |||
| e05f01dce8 | |||
| a29026321f | |||
| f8ee3b92cf | |||
| e0c91214fd | |||
| cc11e63cd7 | |||
| b47c9f81d2 | |||
| fcb93015cc | |||
| fc6355126f | |||
| fd2747d166 | |||
| 3bb22ddc36 | |||
| 0c4d5b5356 | |||
| 61217a944b | |||
| 5434302aa5 | |||
| 23d9c9279c | |||
| 4eae30c022 | |||
| ac7816fc4b | |||
| 10dc7343ae | |||
| a3837083da | |||
| 1644587072 | |||
| 6471141926 | |||
| 65197cf038 | |||
| 785c7e54ab | |||
| 9a2a945762 | |||
| 56eecb0836 | |||
| 8d9cb51baa | |||
| f55049f212 | |||
| dc6093e4fc | |||
| 5df650bc51 | |||
| d23a013579 | |||
| fc5c0eb239 | |||
| 9a42d1d6bd | |||
| 942115acef | |||
| 22b2a62209 | |||
| 0a3a1c9468 | |||
| 655429693a | |||
| 2afd46dc91 | |||
| 9bf755ff25 | |||
| d693cb734b | |||
| 836f48c810 | |||
| 327f2140cf | |||
| e4b605162e | |||
| e952d5c6f8 | |||
| 203e490321 | |||
| 7264e52e6c | |||
| 40bca24dfb | |||
| 56c6ddd392 | |||
| 2cd370d519 | |||
| 42545a027f | |||
| d1ed62ded9 | |||
| 2c9d3abe8c | |||
| a03a3416c2 | |||
| 45138b2bc1 | |||
| 3e57aab717 | |||
| 9aced8f01f | |||
| 3e1e6f7164 | |||
| 5aff88630a | |||
| 7e3555d84a | |||
| 6d7e7f97c7 | |||
| 0785c375a5 | |||
| 0d68eeac63 | |||
| 634e63a3dc | |||
| f60fd9cc63 | |||
| 49628ad3cf | |||
| 13a18eb556 | |||
| 76ec548b4f | |||
| 9c9c82a547 | |||
| b1ccd16870 | |||
| fedf2db847 |
@@ -33,6 +33,9 @@ jobs:
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
- name: setCurrentVersion
|
||||
run: |
|
||||
yarn setCurrentVersion
|
||||
- name: Publish
|
||||
run: |
|
||||
yarn run build:app
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
@@ -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 [](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) [](https://www.npmjs.com/package/dbgate-engines)
|
||||
structure, creating specific queries (JavaScript) [](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) [](https://www.npmjs.com/package/dbgate-sqltree)
|
||||
* [types](https://github.com/dbshell/dbgate/tree/master/packages/types) - common TypeScript definitions [](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 [](https://www.npmjs.com/package/dbgate-tools)
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "3.7.33",
|
||||
"version": "3.8.13",
|
||||
"private": true,
|
||||
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
|
||||
"dependencies": {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix packages-api-v
|
||||
version-git-message "packages-api v%s"
|
||||
+8
-40
@@ -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
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
version: '3.8.6',
|
||||
buildTime: '2020-12-10T11:14:01.053Z'
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix packages-sqlitree-v
|
||||
version-git-message "packages-sqlitree v%s"
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"name": "dbgate-sqltree",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix packages-tools-v
|
||||
version-git-message "packages-tools v%s"
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './createBulkInsertStreamBase';
|
||||
export * from './DatabaseAnalyser';
|
||||
export * from './driverBase';
|
||||
export * from './SqlDumper';
|
||||
export * from './testPermission';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix packages-types-v
|
||||
version-git-message "packages-types v%s"
|
||||
@@ -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
|
||||
|
||||
Vendored
+4
-3
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
+21
-17
@@ -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;
|
||||
+23
-7
@@ -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;
|
||||
+50
-33
@@ -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;
|
||||
+33
-21
@@ -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;
|
||||
+73
-21
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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 & 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 & 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" />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user