Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cb304a41c | |||
| abdbe4122f | |||
| b2e37e88ea | |||
| 6a28ceaa51 | |||
| c15f859eae | |||
| f717dfa3b5 | |||
| ab7510c8e8 | |||
| 3601ac21b4 | |||
| 65bd5a60ef | |||
| 6a9bd8248c | |||
| 98ff6db701 | |||
| 7b67576131 | |||
| d0d6d86bb9 | |||
| 1f057fb0a9 | |||
| 75a429b74f | |||
| d34524c3d0 | |||
| bdfa66d37d | |||
| 1d5d87e26a | |||
| 9f3aadc17d | |||
| 58f213d042 | |||
| 89dbf38962 | |||
| 670e3d127e | |||
| 72181e70a1 | |||
| c3ac836fa9 | |||
| b0deba4bae | |||
| d08fc85459 | |||
| 417ec9fcd2 | |||
| f9d4a9a3a0 | |||
| eab870c237 | |||
| 521199ee1a | |||
| 0d1a6e96f3 | |||
| 1076fb8391 | |||
| 114dc0b543 | |||
| 728ca72cc1 | |||
| e243ecd96a | |||
| 2defdc3f28 | |||
| 7aeef55a58 | |||
| 5e8967da52 | |||
| 679145a394 | |||
| 9b012c187a | |||
| 9ce1fdd59e | |||
| c0d0a00615 | |||
| c67c08bd69 | |||
| 6e846797b9 | |||
| a3ad98d2a9 | |||
| c7dbf333c7 | |||
| 19392e9406 | |||
| 818de9b111 | |||
| 0292a37b16 | |||
| 8b9031b0c2 | |||
| d88591032e | |||
| 2c6a59638b | |||
| 8312415430 | |||
| fdb14d687b | |||
| a20b351938 | |||
| f8016d26ec | |||
| ad186f5efb | |||
| 8e26918975 | |||
| e0303aa77e | |||
| 6a02c4ebaa | |||
| 2c73ab6bc1 | |||
| b60714f30c | |||
| f3163617e0 | |||
| 834be32676 | |||
| 0f6637188b | |||
| ef0921ecf5 | |||
| 1ffa613e09 | |||
| c0c8cd88e3 | |||
| 2666717c3a | |||
| 777abbc097 | |||
| e4db985ef9 | |||
| 6afaa6f856 | |||
| 1325851bcf | |||
| fb7da60127 | |||
| ecde9cb6bd | |||
| 6b06ed5baf | |||
| 2aa965cf3b | |||
| fc11fe1e8d | |||
| e33e14bd5f | |||
| 0c7fc0b7b6 | |||
| 5904d45c44 | |||
| 5d997fc1c9 | |||
| 4693564ffa | |||
| 3377929b30 | |||
| a35f6f2629 | |||
| cb264ac6cc | |||
| 9236e1a6c2 | |||
| df359aea58 | |||
| fdf60b5267 | |||
| bd3c18d883 | |||
| 18bf6e5979 | |||
| edaf9676e4 | |||
| bd524d345a | |||
| 0a39a6829c | |||
| 5c7a011efb | |||
| 4e350e99c4 | |||
| a714f7ae54 | |||
| a17b76c570 | |||
| 54d476a972 | |||
| 255c3e5ef4 | |||
| 059eabf2fa | |||
| 79fdde73ae | |||
| 84e475192e | |||
| 3907b1ae8b | |||
| dcfefc78a2 | |||
| d3039a9248 | |||
| 31dd80b79a | |||
| 8d6d1d979e | |||
| fe1c5f5801 | |||
| df976a84d2 | |||
| 420e94600e | |||
| 9940bd5177 | |||
| 45d99a4126 | |||
| c2b7c775c0 | |||
| 51ba9d3b5a | |||
| 8396e726ec | |||
| cb67b57faf | |||
| 99381536d7 | |||
| a9cb9f1874 | |||
| 420a58380a | |||
| 75ca3cbb11 | |||
| a5c1966a94 | |||
| ca4ff95316 |
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve DbGate
|
||||
title: 'BUG: Say something here'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Version Information (please complete the following information):**
|
||||
- OS: [e.g. Windows/Mac/Ubuntu]
|
||||
- App Version [see Help -> About]
|
||||
- Install source [e.g. installer/SNAP/Docker/NPM]
|
||||
- Type - Web/Application
|
||||
- Database engine: [e.g. MySQL/PostgreSQL/SQL Server]
|
||||
|
||||
**Additional context**
|
||||
Anything else you think might be helpful
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for DbGate
|
||||
title: 'FEAT: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question about how to do something
|
||||
title: 'QUESTION: Summary of your question'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Details:**
|
||||
Details about your question
|
||||
|
||||
**Version Information [might be relevant to your issue]**
|
||||
Operating System:
|
||||
App Version [help -> about]:
|
||||
|
||||
**Screenshot [if appropriate]**:
|
||||
A screenshot of the app if that helps
|
||||
@@ -0,0 +1,91 @@
|
||||
name: NPM packages
|
||||
|
||||
# on: [push]
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
# - 'v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+'
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches:
|
||||
# - production
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
# os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Use Node.js 10.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
|
||||
- name: Configure NPM token
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
|
||||
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
|
||||
- name: setCurrentVersion
|
||||
run: |
|
||||
yarn setCurrentVersion
|
||||
|
||||
- name: Publish types
|
||||
working-directory: packages/types
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish tools
|
||||
working-directory: packages/tools
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish sqltree
|
||||
working-directory: packages/sqltree
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish api
|
||||
working-directory: packages/api
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish datalib
|
||||
working-directory: packages/datalib
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish filterparser
|
||||
working-directory: packages/filterparser
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish web
|
||||
working-directory: packages/web
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate
|
||||
working-directory: packages/dbgate
|
||||
run: |
|
||||
npm publish
|
||||
@@ -0,0 +1,13 @@
|
||||
# ChangeLog
|
||||
|
||||
### 3.9.6
|
||||
- ADDED: Connect using SSH Tunnel
|
||||
- ADDED: Connect using SSL
|
||||
- ADDED: Database connection dialog redesigned
|
||||
- ADDED: #63 Ctrl+Enter runs query
|
||||
- ADDED: Published dbgate NPM package
|
||||
- ADDED: SQL editor context menu
|
||||
- FIX: #62 - import, export executed from SNAP installs didn't work
|
||||
|
||||
### 3.9.5
|
||||
- Start point of changelog
|
||||
@@ -1,32 +1,41 @@
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
[](https://www.npmjs.com/package/dbgate-api)
|
||||
|
||||
# DbGate - database administration tool
|
||||
|
||||
DbGate is fast and efficient database administration tool. It is focused to work with data (filtering, editing, master/detail views etc.)
|
||||
DbGate is fast and easy to use database manager. Works with MySQL, PostgreSQL and SQL Server.
|
||||
|
||||
**Try it online** - https://demo.dbgate.org - online demo application
|
||||
* Try it online - [demo.dbgate.org](https://demo.dbgate.org) - online demo application
|
||||
* Download application for Windows, Linux or Mac from [dbgate.org](https://dbgate.org/download/)
|
||||
* Run web version as [NPM package](https://www.npmjs.com/package/dbgate) or as [docker image](https://hub.docker.com/r/dbgate/dbgate)
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
* Support for Microsoft SQL Server, Postgre SQL, MySQL
|
||||
* Table data browsing - filtering, sorting, related columns using foreign keys
|
||||
* Connect to Microsoft SQL Server, Postgre SQL, MySQL
|
||||
* Table data editing, with SQL change script preview
|
||||
* Master/detail views
|
||||
* Query designer
|
||||
* Form view for comfortable work with tables with many columns
|
||||
* Charts
|
||||
* Browsing objects - tables, views, procedures, functions
|
||||
* Table data editing, with SQL change script preview
|
||||
* Explore tables, views, procedures, functions
|
||||
* SQL editor, execute SQL script, SQL code formatter, SQL code completion, SQL join wizard
|
||||
* Runs as application for Windows, Linux and Mac. Or in Docker container on server and in web Browser on client.
|
||||
* Import, export from/to CSV, Excel, JSON
|
||||
* Free table editor - quick table data editing (cleanup data after import/before export, prototype tables etc.)
|
||||
* Archives - backup your data in JSON files on local filesystem (or on DbGate server, when using web application)
|
||||
* Light and dark theme
|
||||
* Charts
|
||||
* For detailed info, how to run DbGate in docker container, visit [docker hub](https://hub.docker.com/r/dbgate/dbgate)
|
||||
* Extensible plugin architecture
|
||||
|
||||

|
||||
## Why is DbGate different
|
||||
There are many database managers now, so why DbGate?
|
||||
* Works everywhere - Windows, Linux, Mac, Web browser (+mobile web is planned), without compromises in features
|
||||
* Based on standalone NPM packages, scripts can be run without DbGate (example - [CSV export](https://www.npmjs.com/package/dbgate-plugin-csv) )
|
||||
* Many data browsing functions based using foreign keys - master/detail, expand columns, expandable form view (on screenshot above)
|
||||
|
||||
## Design goals
|
||||
* Application simplicity - DbGate takes the best and only the best from old [DbGate](http://www.jenasoft.com/dbgate), [DatAdmin](http://www.jenasoft.com/datadmin) and [DbMouse](http://www.jenasoft.com/dbmouse) .
|
||||
@@ -41,36 +50,12 @@ DbGate is fast and efficient database administration tool. It is focused to work
|
||||
## 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.
|
||||
Visit [dbgate generator homepage](https://github.com/dbgate/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 or database engines
|
||||
* Bug fixing
|
||||
* Test Mac edition
|
||||
* Create unit tests
|
||||
* Whatever else
|
||||
|
||||
Any help is appreciated!
|
||||
|
||||
Feel free to report issues and open merge requests.
|
||||
|
||||
## Roadmap
|
||||
|
||||
| Feature | Complexity | Schedule |
|
||||
|---|---|---|
|
||||
| Table designer (structure editor) | big | february 2021 |
|
||||
| Support for SQLite | big | 2021 |
|
||||
| Filtering, sorting in free table editor | small | ??? |
|
||||
| Query designer | medium | december 2020 - done |
|
||||
| Using tedious driver instead of mssql | small | january 2021 - done |
|
||||
| Filter SQL result sets | small | november 2020 - done |
|
||||
|
||||
## How to run development environment
|
||||
|
||||
```sh
|
||||
@@ -85,7 +70,7 @@ yarn lib
|
||||
|
||||
Open http://localhost:5000 in your browser
|
||||
|
||||
You could run electron app, using this server:
|
||||
You could run electron app (requires running localhost:5000):
|
||||
```sh
|
||||
cd app
|
||||
yarn
|
||||
@@ -93,7 +78,7 @@ yarn start
|
||||
```
|
||||
|
||||
## How to run built electron app locally
|
||||
This mode is very similar to production run of electron app. Electron app forks process with API on dynamically allocated port, works with compiled javascript files.
|
||||
This mode is very similar to production run of electron app. Electron app forks process with API on dynamically allocated port, works with compiled javascript files (doesn't use localhost:5000)
|
||||
|
||||
```sh
|
||||
cd app
|
||||
@@ -109,13 +94,12 @@ yarn start:app:local
|
||||
## Packages
|
||||
Some dbgate packages can be used also without DbGate. You can find them on [NPM repository](https://www.npmjs.com/search?q=keywords:dbgate)
|
||||
|
||||
* [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)
|
||||
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)
|
||||
* [api](https://github.com/dbgate/dbgate/tree/master/packages/api) - backend, Javascript, ExpressJS [](https://www.npmjs.com/package/dbgate-api)
|
||||
* [datalib](https://github.com/dbgate/dbgate/tree/master/packages/datalib) - TypeScript library for utility classes [](https://www.npmjs.com/package/dbgate-datalib)
|
||||
* [app](https://github.com/dbgate/dbgate/tree/master/app) - application (JavaScript) structure, creating specific queries (JavaScript)
|
||||
* [filterparser](https://github.com/dbgate/dbgate/tree/master/packages/filterparser) - TypeScript library for parsing data filter expressions using parsimmon [](https://www.npmjs.com/package/dbgate-filterparser)
|
||||
* [sqltree](https://github.com/dbgate/dbgate/tree/master/packages/sqltree) - JSON representation of SQL query, functions converting to SQL (TypeScript) [](https://www.npmjs.com/package/dbgate-sqltree)
|
||||
* [types](https://github.com/dbgate/dbgate/tree/master/packages/types) - common TypeScript definitions [](https://www.npmjs.com/package/dbgate-types)
|
||||
* [web](https://github.com/dbgate/dbgate/tree/master/packages/web) - frontend in React (JavaScript) [](https://www.npmjs.com/package/dbgate-web)
|
||||
* [tools](https://github.com/dbgate/dbgate/tree/master/packages/tools) - various tools [](https://www.npmjs.com/package/dbgate-tools)
|
||||
|
||||
|
||||
+13
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "3.9.3",
|
||||
"version": "3.9.5",
|
||||
"private": true,
|
||||
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
|
||||
"description": "Opensource database administration tool",
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.dbgate",
|
||||
@@ -37,6 +37,15 @@
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"snap": {
|
||||
"publish": [
|
||||
"github",
|
||||
"snapStore"
|
||||
],
|
||||
"environment": {
|
||||
"ELECTRON_SNAP": "true"
|
||||
}
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
@@ -68,10 +77,10 @@
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"electron": "11.1.1",
|
||||
"electron": "11.2.3",
|
||||
"electron-builder": "22.9.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
-21
@@ -1,6 +1,6 @@
|
||||
const electron = require('electron');
|
||||
const os = require('os');
|
||||
const { Menu } = require('electron');
|
||||
const { Menu, ipcMain } = require('electron');
|
||||
const { fork } = require('child_process');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const Store = require('electron-store');
|
||||
@@ -20,6 +20,7 @@ const store = new Store();
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
let splashWindow;
|
||||
let mainMenu;
|
||||
|
||||
log.transports.file.level = 'debug';
|
||||
autoUpdater.logger = log;
|
||||
@@ -45,18 +46,63 @@ function buildMenu() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Open file',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_openFile()`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('save')`);
|
||||
},
|
||||
accelerator: 'Ctrl+S',
|
||||
id: 'save',
|
||||
},
|
||||
{
|
||||
label: 'Save As',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('saveAs')`);
|
||||
},
|
||||
accelerator: 'Ctrl+Shift+S',
|
||||
id: 'saveAs',
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{
|
||||
label: 'New query',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Close all tabs',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
|
||||
},
|
||||
},
|
||||
{ role: 'minimize' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [{ role: 'copy' }, { role: 'paste' }],
|
||||
},
|
||||
|
||||
// {
|
||||
// label: 'Edit',
|
||||
// submenu: [
|
||||
// { role: 'undo' },
|
||||
// { role: 'redo' },
|
||||
// { type: 'separator' },
|
||||
// { role: 'cut' },
|
||||
// { role: 'copy' },
|
||||
// { role: 'paste' },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
@@ -71,20 +117,6 @@ function buildMenu() {
|
||||
{ role: 'togglefullscreen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Close all tabs',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'minimize' },
|
||||
{ role: 'close' },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
@@ -97,7 +129,7 @@ function buildMenu() {
|
||||
{
|
||||
label: 'DbGate on GitHub',
|
||||
click() {
|
||||
require('electron').shell.openExternal('https://github.com/dbshell/dbgate');
|
||||
require('electron').shell.openExternal('https://github.com/dbgate/dbgate');
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -106,6 +138,12 @@ function buildMenu() {
|
||||
require('electron').shell.openExternal('https://hub.docker.com/r/dbgate/dbgate');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Report problem or feature request',
|
||||
click() {
|
||||
require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
click() {
|
||||
@@ -119,6 +157,12 @@ function buildMenu() {
|
||||
return Menu.buildFromTemplate(template);
|
||||
}
|
||||
|
||||
ipcMain.on('update-menu', async (event, arg) => {
|
||||
const commands = await mainWindow.webContents.executeJavaScript(`dbgate_getCurrentTabCommands()`);
|
||||
mainMenu.getMenuItemById('save').enabled = !!commands.save;
|
||||
mainMenu.getMenuItemById('saveAs').enabled = !!commands.saveAs;
|
||||
});
|
||||
|
||||
function createWindow() {
|
||||
const bounds = store.get('winBounds');
|
||||
|
||||
@@ -135,7 +179,8 @@ function createWindow() {
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.setMenu(buildMenu());
|
||||
mainMenu = buildMenu();
|
||||
mainWindow.setMenu(mainMenu);
|
||||
|
||||
function loadMainWindow() {
|
||||
const startUrl =
|
||||
|
||||
+4
-4
@@ -717,10 +717,10 @@ electron-updater@^4.3.5:
|
||||
lodash.isequal "^4.5.0"
|
||||
semver "^7.3.2"
|
||||
|
||||
electron@11.1.1:
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-11.1.1.tgz#188f036f8282798398dca9513e9bb3b10213e3aa"
|
||||
integrity sha512-tlbex3xosJgfileN6BAQRotevPRXB/wQIq48QeQ08tUJJrXwE72c8smsM/hbHx5eDgnbfJ2G3a60PmRjHU2NhA==
|
||||
electron@11.2.3:
|
||||
version "11.2.3"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-11.2.3.tgz#8ad1d9858436cfca0e2e5ea7fea326794ae58ebb"
|
||||
integrity sha512-6yxOc42nDAptHKNlUG/vcOh2GI9x2fqp2nQbZO0/3sz2CrwsJkwR3i3oMN9XhVJaqI7GK1vSCJz0verOkWlXcQ==
|
||||
dependencies:
|
||||
"@electron/get" "^1.0.1"
|
||||
"@types/node" "^12.0.12"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "3.9.6",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
@@ -32,7 +32,7 @@ dbgateApi.runScript(run);
|
||||
|
||||
```
|
||||
|
||||
Silly example, runs without any dependencies. Copy [fakeObjectReader](https://github.com/dbshell/dbgate/blob/master/packages/api/src/shell/fakeObjectReader.js) to [consoleObjectWriter](https://github.com/dbshell/dbgate/blob/master/packages/api/src/shell/consoleObjectWriter.js) .
|
||||
Silly example, runs without any dependencies. Copy [fakeObjectReader](https://github.com/dbgate/dbgate/blob/master/packages/api/src/shell/fakeObjectReader.js) to [consoleObjectWriter](https://github.com/dbgate/dbgate/blob/master/packages/api/src/shell/consoleObjectWriter.js) .
|
||||
|
||||
```javascript
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "dbgate-api",
|
||||
"main": "src/index.js",
|
||||
"version": "1.0.7",
|
||||
"version": "3.9.5",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "GPL",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"sql",
|
||||
"json",
|
||||
@@ -26,8 +26,8 @@
|
||||
"compare-versions": "^3.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-sqltree": "^1.0.0",
|
||||
"dbgate-tools": "^1.0.0",
|
||||
"dbgate-sqltree": "^3.9.5",
|
||||
"dbgate-tools": "^3.9.5",
|
||||
"eslint": "^6.8.0",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
@@ -35,12 +35,16 @@
|
||||
"find-free-port": "^2.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"http": "^0.0.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"line-reader": "^0.4.0",
|
||||
"lodash": "^4.17.15",
|
||||
"ncp": "^2.0.0",
|
||||
"nedb-promises": "^4.0.1",
|
||||
"node-cron": "^2.0.3",
|
||||
"node-ssh-forward": "^0.7.2",
|
||||
"portfinder": "^1.0.28",
|
||||
"simple-encryptor": "^4.0.0",
|
||||
"socket.io": "^2.3.0",
|
||||
"tar": "^6.0.5",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
@@ -53,7 +57,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.149",
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"env-cmd": "^10.1.0",
|
||||
"node-loader": "^1.0.2",
|
||||
"nodemon": "^2.0.2",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const currentVersion = require('../currentVersion');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
|
||||
module.exports = {
|
||||
get_meta: 'get',
|
||||
@@ -31,4 +32,10 @@ module.exports = {
|
||||
...currentVersion,
|
||||
};
|
||||
},
|
||||
|
||||
platformInfo_meta: 'get',
|
||||
async platformInfo() {
|
||||
return platformInfo;
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ const nedb = require('nedb-promises');
|
||||
const { datadir } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
const { encryptConnection } = require('../utility/crypting');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
function getPortalCollections() {
|
||||
if (process.env.CONNECTIONS) {
|
||||
@@ -47,6 +48,7 @@ module.exports = {
|
||||
test(req, res) {
|
||||
const subprocess = fork(process.argv[1], ['connectProcess', ...process.argv.slice(3)]);
|
||||
subprocess.on('message', resp => {
|
||||
if (handleProcessCommunication(resp, subprocess)) return;
|
||||
// @ts-ignore
|
||||
const { msgtype } = resp;
|
||||
if (msgtype == 'connected' || msgtype == 'error') {
|
||||
|
||||
@@ -3,6 +3,7 @@ const connections = require('./connections');
|
||||
const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const { DatabaseAnalyser } = require('dbgate-tools');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
|
||||
@@ -50,8 +51,10 @@ module.exports = {
|
||||
status: { name: 'pending' },
|
||||
};
|
||||
this.opened.push(newOpened);
|
||||
// @ts-ignore
|
||||
subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, subprocess)) return;
|
||||
if (newOpened.disconnected) return;
|
||||
this[`handle_${msgtype}`](conid, database, message);
|
||||
});
|
||||
|
||||
@@ -78,6 +78,11 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
saveAs_meta: 'post',
|
||||
async saveAs({ filePath, data, format }) {
|
||||
await fs.writeFile(filePath, serialize(format, data));
|
||||
},
|
||||
|
||||
favorites_meta: 'get',
|
||||
async favorites() {
|
||||
if (!hasPermission(`files/favorites/read`)) return [];
|
||||
|
||||
@@ -28,9 +28,9 @@ const hasPermission = require('../utility/hasPermission');
|
||||
// }
|
||||
|
||||
const preinstallPluginMinimalVersions = {
|
||||
'dbgate-plugin-mssql': '1.0.10',
|
||||
'dbgate-plugin-mysql': '1.0.3',
|
||||
'dbgate-plugin-postgres': '1.0.2',
|
||||
'dbgate-plugin-mssql': '1.1.0',
|
||||
'dbgate-plugin-mysql': '1.1.0',
|
||||
'dbgate-plugin-postgres': '1.1.0',
|
||||
'dbgate-plugin-csv': '1.0.8',
|
||||
'dbgate-plugin-excel': '1.0.6',
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const { rundir, uploadsdir, pluginsdir } = require('../utility/directories');
|
||||
const { extractShellApiPlugins, extractShellApiFunctionName } = require('dbgate-tools');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
function extractPlugins(script) {
|
||||
const requireRegex = /\s*\/\/\s*@require\s+([^\s]+)\s*\n/g;
|
||||
@@ -23,6 +24,7 @@ const requirePluginsTemplate = plugins =>
|
||||
|
||||
const scriptTemplate = script => `
|
||||
const dbgateApi = require(process.env.DBGATE_API);
|
||||
dbgateApi.initializeApiEnvironment();
|
||||
${requirePluginsTemplate(extractPlugins(script))}
|
||||
require=null;
|
||||
async function run() {
|
||||
@@ -35,6 +37,7 @@ dbgateApi.runScript(run);
|
||||
|
||||
const loaderScriptTemplate = (functionName, props, runid) => `
|
||||
const dbgateApi = require(process.env.DBGATE_API);
|
||||
dbgateApi.initializeApiEnvironment();
|
||||
${requirePluginsTemplate(extractShellApiPlugins(functionName, props))}
|
||||
require=null;
|
||||
async function run() {
|
||||
@@ -96,7 +99,8 @@ module.exports = {
|
||||
cwd: directory,
|
||||
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
||||
env: {
|
||||
DBGATE_API: process.argv[1],
|
||||
...process.env,
|
||||
DBGATE_API: global['dbgateApiModulePath'] || process.argv[1],
|
||||
..._.fromPairs(pluginNames.map(name => [`PLUGIN_${_.camelCase(name)}`, path.join(pluginsdir(), name)])),
|
||||
},
|
||||
});
|
||||
@@ -123,8 +127,10 @@ module.exports = {
|
||||
subprocess,
|
||||
};
|
||||
this.opened.push(newOpened);
|
||||
// @ts-ignore
|
||||
subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, subprocess)) return;
|
||||
this[`handle_${msgtype}`](runid, message);
|
||||
});
|
||||
return newOpened;
|
||||
|
||||
@@ -3,11 +3,13 @@ const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const _ = require('lodash');
|
||||
const AsyncLock = require('async-lock');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const lock = new AsyncLock();
|
||||
|
||||
module.exports = {
|
||||
opened: [],
|
||||
closed: {},
|
||||
lastPinged: {},
|
||||
|
||||
handle_databases(conid, { databases }) {
|
||||
const existing = this.opened.find(x => x.conid == conid);
|
||||
@@ -42,8 +44,10 @@ module.exports = {
|
||||
this.opened.push(newOpened);
|
||||
delete this.closed[conid];
|
||||
socket.emitChanged(`server-status-changed`);
|
||||
// @ts-ignore
|
||||
subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, subprocess)) return;
|
||||
if (newOpened.disconnected) return;
|
||||
this[`handle_${msgtype}`](conid, message);
|
||||
});
|
||||
@@ -88,7 +92,12 @@ module.exports = {
|
||||
ping_meta: 'post',
|
||||
async ping({ connections }) {
|
||||
await Promise.all(
|
||||
connections.map(async conid => {
|
||||
_.uniq(connections).map(async conid => {
|
||||
const last = this.lastPinged[conid];
|
||||
if (last && new Date().getTime() - last < 30 * 1000) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.lastPinged[conid] = new Date().getTime();
|
||||
const opened = await this.ensureOpened(conid);
|
||||
opened.subprocess.send({ msgtype: 'ping' });
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ const connections = require('./connections');
|
||||
const socket = require('../utility/socket');
|
||||
const { fork } = require('child_process');
|
||||
const jsldata = require('./jsldata');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('dbgate-types').OpenedSession[]} */
|
||||
@@ -73,8 +74,10 @@ module.exports = {
|
||||
sesid,
|
||||
};
|
||||
this.opened.push(newOpened);
|
||||
// @ts-ignore
|
||||
subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, subprocess)) return;
|
||||
this[`handle_${msgtype}`](sesid, message);
|
||||
});
|
||||
subprocess.send({ msgtype: 'connect', ...connection, database });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
version: '3.8.6',
|
||||
buildTime: '2020-12-10T11:14:01.053Z',
|
||||
|
||||
module.exports = {
|
||||
version: '3.9.5',
|
||||
buildTime: '2021-02-08T18:21:44.182Z'
|
||||
};
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
const shell = require('./shell');
|
||||
// require('socket.io-client');
|
||||
|
||||
// "socket.io-client": "^2.3.0",
|
||||
// "utf-8-validate": "^5.0.2",
|
||||
// "uuid": "^3.4.0",
|
||||
// "uws": "10.148.1"
|
||||
|
||||
const argument = process.argv[2];
|
||||
if (argument && argument.endsWith('Process')) {
|
||||
@@ -18,4 +12,7 @@ if (argument && argument.endsWith('Process')) {
|
||||
main.start(argument);
|
||||
}
|
||||
|
||||
module.exports = shell;
|
||||
module.exports = {
|
||||
...shell,
|
||||
getMainModule: () => require('./main'),
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ const io = require('socket.io');
|
||||
const fs = require('fs');
|
||||
const findFreePort = require('find-free-port');
|
||||
const childProcessChecker = require('./utility/childProcessChecker');
|
||||
const path = require('path');
|
||||
|
||||
const useController = require('./utility/useController');
|
||||
const socket = require('./utility/socket');
|
||||
@@ -82,9 +83,11 @@ function start(argument = null) {
|
||||
// server static files inside docker container
|
||||
app.use(express.static('/home/dbgate-docker/build'));
|
||||
} else {
|
||||
app.get('/', (req, res) => {
|
||||
res.send('DbGate API');
|
||||
});
|
||||
if (argument != 'startNodeWeb') {
|
||||
app.get('/', (req, res) => {
|
||||
res.send('DbGate API');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (argument == '--dynport') {
|
||||
@@ -96,6 +99,13 @@ function start(argument = null) {
|
||||
process.send({ msgtype: 'listening', port });
|
||||
});
|
||||
});
|
||||
} else if (argument == 'startNodeWeb') {
|
||||
app.use(express.static(path.join(__dirname, '../../dbgate-web/build')));
|
||||
findFreePort(5000, function (err, port) {
|
||||
server.listen(port, () => {
|
||||
console.log(`DbGate API listening on port ${port}`);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
server.listen(3000);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
function start() {
|
||||
childProcessChecker();
|
||||
process.on('message', async connection => {
|
||||
if (handleProcessCommunication(connection)) return;
|
||||
try {
|
||||
const driver = requireEngineDriver(connection);
|
||||
const conn = await driver.connect(decryptConnection(connection));
|
||||
const conn = await connectUtility(driver, connection);
|
||||
const res = await driver.getVersion(conn);
|
||||
process.send({ msgtype: 'connected', ...res });
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
let systemConnection;
|
||||
let storedConnection;
|
||||
@@ -60,7 +61,7 @@ async function handleConnect({ connection, structure }) {
|
||||
|
||||
if (!structure) setStatusName('pending');
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await checkedAsyncCall(driver.connect(decryptConnection(storedConnection)));
|
||||
systemConnection = await checkedAsyncCall(connectUtility(driver, storedConnection));
|
||||
if (structure) {
|
||||
analysedStructure = structure;
|
||||
handleIncrementalRefresh();
|
||||
@@ -127,6 +128,7 @@ function start() {
|
||||
}, 60 * 1000);
|
||||
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
try {
|
||||
await handleMessage(message);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const JsonLinesDatastore = require('../utility/JsonLinesDatastore');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
let lastPing = null;
|
||||
let datastore = new JsonLinesDatastore();
|
||||
@@ -47,6 +48,7 @@ function start() {
|
||||
}, 60 * 1000);
|
||||
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
try {
|
||||
await handleMessage(message);
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,6 +2,8 @@ const stableStringify = require('json-stable-stringify');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
let systemConnection;
|
||||
let storedConnection;
|
||||
@@ -48,7 +50,7 @@ async function handleConnect(connection) {
|
||||
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
systemConnection = await driver.connect(decryptConnection(storedConnection));
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
handleRefresh();
|
||||
setInterval(handleRefresh, 30 * 1000);
|
||||
} catch (err) {
|
||||
@@ -67,7 +69,7 @@ function handlePing() {
|
||||
|
||||
async function handleCreateDatabase({ name }) {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await driver.connect(decryptConnection(storedConnection));
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
console.log(`RUNNING SCRIPT: CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
|
||||
await driver.query(systemConnection, `CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
|
||||
await handleRefresh();
|
||||
@@ -96,6 +98,7 @@ function start() {
|
||||
}, 60 * 1000);
|
||||
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
try {
|
||||
await handleMessage(message);
|
||||
} catch (err) {
|
||||
|
||||
@@ -8,6 +8,8 @@ const goSplit = require('../utility/goSplit');
|
||||
const { jsldir } = require('../utility/directories');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
let systemConnection;
|
||||
let storedConnection;
|
||||
@@ -131,7 +133,7 @@ async function handleConnect(connection) {
|
||||
storedConnection = connection;
|
||||
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await driver.connect(decryptConnection(storedConnection));
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
for (const [resolve] of afterConnectCallbacks) {
|
||||
resolve();
|
||||
}
|
||||
@@ -182,6 +184,7 @@ async function handleMessage({ msgtype, ...other }) {
|
||||
function start() {
|
||||
childProcessChecker();
|
||||
process.on('message', async message => {
|
||||
if (handleProcessCommunication(message)) return;
|
||||
try {
|
||||
await handleMessage(message);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
const goSplit = require('../utility/goSplit');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function executeQuery({ connection, sql }) {
|
||||
console.log(`Execute query ${sql}`);
|
||||
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(decryptConnection(connection));
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
|
||||
for (const sqlItem of goSplit(sql)) {
|
||||
|
||||
@@ -17,6 +17,7 @@ const requirePlugin = require('./requirePlugin');
|
||||
const download = require('./download');
|
||||
const executeQuery = require('./executeQuery');
|
||||
const loadFile = require('./loadFile');
|
||||
const initializeApiEnvironment = require('./initializeApiEnvironment');
|
||||
|
||||
const dbgateApi = {
|
||||
queryReader,
|
||||
@@ -37,6 +38,7 @@ const dbgateApi = {
|
||||
registerPlugins,
|
||||
executeQuery,
|
||||
loadFile,
|
||||
initializeApiEnvironment,
|
||||
};
|
||||
|
||||
requirePlugin.initializeDbgateApi(dbgateApi);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
async function initializeApiEnvironment() {
|
||||
process.on('message', async message => {
|
||||
handleProcessCommunication(message);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = initializeApiEnvironment;
|
||||
@@ -1,11 +1,12 @@
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function queryReader({ connection, sql }) {
|
||||
console.log(`Reading query ${sql}`);
|
||||
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(decryptConnection(connection));
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
return await driver.readQuery(pool, sql);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const { quoteFullName, fullNameToString } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function tableReader({ connection, pureName, schemaName }) {
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(decryptConnection(connection));
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
|
||||
const fullName = { pureName, schemaName };
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
const { fullNameToString } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function tableWriter({ connection, schemaName, pureName, ...options }) {
|
||||
console.log(`Writing table ${fullNameToString({ schemaName, pureName })}`);
|
||||
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(decryptConnection(connection));
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
return await driver.writeTable(pool, { schemaName, pureName }, options);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { fork } = require('child_process');
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const { handleProcessCommunication } = require('./processComm');
|
||||
|
||||
class DatastoreProxy {
|
||||
constructor(file) {
|
||||
@@ -30,8 +31,11 @@ class DatastoreProxy {
|
||||
if (!this.subprocess) {
|
||||
this.subprocess = fork(process.argv[1], ['jslDatastoreProcess', ...process.argv.slice(3)]);
|
||||
|
||||
// @ts-ignore
|
||||
this.subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
this.subprocess.on('message', message => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = message;
|
||||
if (handleProcessCommunication(message, this.subprocess)) return;
|
||||
|
||||
// if (this.disconnected) return;
|
||||
this[`handle_${msgtype}`](message);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
const { SSHConnection } = require('node-ssh-forward');
|
||||
const portfinder = require('portfinder');
|
||||
const fs = require('fs-extra');
|
||||
const { decryptConnection } = require('./crypting');
|
||||
const { getSshTunnel } = require('./sshTunnel');
|
||||
const { getSshTunnelProxy } = require('./sshTunnelProxy');
|
||||
|
||||
async function connectUtility(driver, storedConnection) {
|
||||
const connection = {
|
||||
...decryptConnection(storedConnection),
|
||||
};
|
||||
|
||||
if (!connection.port && driver.defaultPort) connection.port = driver.defaultPort.toString();
|
||||
|
||||
if (connection.useSshTunnel) {
|
||||
const tunnel = await getSshTunnelProxy(connection);
|
||||
if (tunnel.state == 'error') {
|
||||
throw new Error(tunnel.message);
|
||||
}
|
||||
|
||||
connection.server = '127.0.0.1';
|
||||
connection.port = tunnel.localPort;
|
||||
}
|
||||
|
||||
// SSL functionality - copied from https://github.com/beekeeper-studio/beekeeper-studio
|
||||
if (connection.useSsl) {
|
||||
connection.ssl = {};
|
||||
|
||||
if (connection.sslCaFile) {
|
||||
connection.ssl.ca = await fs.readFile(connection.sslCaFile);
|
||||
}
|
||||
|
||||
if (connection.sslCertFile) {
|
||||
connection.ssl.cert = await fs.readFile(connection.sslCertFile);
|
||||
}
|
||||
|
||||
if (connection.sslKeyFile) {
|
||||
connection.ssl.key = await fs.readFile(connection.sslKeyFile);
|
||||
}
|
||||
|
||||
if (!connection.ssl.key && !connection.ssl.ca && !connection.ssl.cert) {
|
||||
// TODO: provide this as an option in settings
|
||||
// or per-connection as 'reject self-signed certs'
|
||||
// How it works:
|
||||
// if false, cert can be self-signed
|
||||
// if true, has to be from a public CA
|
||||
// Heroku certs are self-signed.
|
||||
// if you provide ca/cert/key files, it overrides this
|
||||
connection.ssl.rejectUnauthorized = false;
|
||||
} else {
|
||||
connection.ssl.rejectUnauthorized = connection.sslRejectUnauthorized;
|
||||
}
|
||||
}
|
||||
|
||||
const conn = await driver.connect(connection);
|
||||
return conn;
|
||||
}
|
||||
|
||||
module.exports = connectUtility;
|
||||
@@ -42,28 +42,42 @@ function getEncryptor() {
|
||||
return _encryptor;
|
||||
}
|
||||
|
||||
function encryptConnection(connection) {
|
||||
function encryptPasswordField(connection, field) {
|
||||
if (
|
||||
connection &&
|
||||
connection.password &&
|
||||
!connection.password.startsWith('crypt:') &&
|
||||
connection[field] &&
|
||||
!connection[field].startsWith('crypt:') &&
|
||||
connection.passwordMode != 'saveRaw'
|
||||
) {
|
||||
return {
|
||||
...connection,
|
||||
password: 'crypt:' + getEncryptor().encrypt(connection.password),
|
||||
[field]: 'crypt:' + getEncryptor().encrypt(connection[field]),
|
||||
};
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
function decryptPasswordField(connection, field) {
|
||||
if (connection && connection[field] && connection[field].startsWith('crypt:')) {
|
||||
return {
|
||||
...connection,
|
||||
[field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)),
|
||||
};
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
function encryptConnection(connection) {
|
||||
connection = encryptPasswordField(connection, 'password');
|
||||
connection = encryptPasswordField(connection, 'sshPassword');
|
||||
connection = encryptPasswordField(connection, 'sshKeyFilePassword');
|
||||
return connection;
|
||||
}
|
||||
|
||||
function decryptConnection(connection) {
|
||||
if (connection && connection.password && connection.password.startsWith('crypt:')) {
|
||||
return {
|
||||
...connection,
|
||||
password: getEncryptor().decrypt(connection.password.substring('crypt:'.length)),
|
||||
};
|
||||
}
|
||||
connection = decryptPasswordField(connection, 'password');
|
||||
connection = decryptPasswordField(connection, 'sshPassword');
|
||||
connection = decryptPasswordField(connection, 'sshKeyFilePassword');
|
||||
return connection;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const p = process;
|
||||
const platform = p.env.OS_OVERRIDE ? p.env.OS_OVERRIDE : p.platform;
|
||||
const isWindows = platform === 'win32';
|
||||
const isMac = platform === 'darwin';
|
||||
const isLinux = platform === 'linux';
|
||||
const isDocker = fs.existsSync('/home/dbgate-docker/build');
|
||||
|
||||
const platformInfo = {
|
||||
isWindows,
|
||||
isMac,
|
||||
isLinux,
|
||||
isDocker,
|
||||
isSnap: p.env.ELECTRON_SNAP == 'true',
|
||||
isPortable: isWindows && p.env.PORTABLE_EXECUTABLE_DIR,
|
||||
isAppImage: p.env.DESKTOPINTEGRATION === 'AppImageLauncher',
|
||||
sshAuthSock: p.env.SSH_AUTH_SOCK,
|
||||
environment: process.env.NODE_ENV,
|
||||
platform,
|
||||
runningInWebpack: !!p.env.WEBPACK_DEV_SERVER_URL,
|
||||
defaultKeyFile: path.join(os.homedir(), '.ssh/id_rsa'),
|
||||
};
|
||||
|
||||
module.exports = platformInfo;
|
||||
@@ -0,0 +1,18 @@
|
||||
const { handleGetSshTunnelRequest, handleGetSshTunnelResponse } = require('./sshTunnelProxy');
|
||||
|
||||
function handleProcessCommunication(message, subprocess) {
|
||||
const { msgtype } = message;
|
||||
if (msgtype == 'getsshtunnel-request') {
|
||||
handleGetSshTunnelRequest(message, subprocess);
|
||||
return true;
|
||||
}
|
||||
if (msgtype == 'getsshtunnel-response') {
|
||||
handleGetSshTunnelResponse(message, subprocess);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleProcessCommunication,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
const { SSHConnection } = require('node-ssh-forward');
|
||||
const fs = require('fs-extra');
|
||||
const portfinder = require('portfinder');
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const _ = require('lodash');
|
||||
const platformInfo = require('./platformInfo');
|
||||
|
||||
const sshConnectionCache = {};
|
||||
const sshTunnelCache = {};
|
||||
|
||||
const CONNECTION_FIELDS = [
|
||||
'sshHost',
|
||||
'sshPort',
|
||||
'sshLogin',
|
||||
'sshPassword',
|
||||
'sshMode',
|
||||
'sshKeyFile',
|
||||
'sshBastionHost',
|
||||
'sshKeyFilePassword',
|
||||
];
|
||||
const TUNNEL_FIELDS = [...CONNECTION_FIELDS, 'server', 'port'];
|
||||
|
||||
async function getSshConnection(connection) {
|
||||
const connectionCacheKey = stableStringify(_.pick(connection, CONNECTION_FIELDS));
|
||||
if (sshConnectionCache[connectionCacheKey]) return sshConnectionCache[connectionCacheKey];
|
||||
|
||||
const sshConfig = {
|
||||
endHost: connection.sshHost || '',
|
||||
endPort: connection.sshPort || 22,
|
||||
bastionHost: connection.sshBastionHost || '',
|
||||
agentForward: connection.sshMode == 'agent',
|
||||
passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyFilePassword : undefined,
|
||||
username: connection.sshLogin,
|
||||
password: connection.sshMode == 'userPassword' ? connection.sshPassword : undefined,
|
||||
agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined,
|
||||
privateKey:
|
||||
connection.sshMode == 'keyFile' && connection.sshKeyFile ? await fs.readFile(connection.sshKeyFile) : undefined,
|
||||
skipAutoPrivateKey: true,
|
||||
noReadline: true,
|
||||
};
|
||||
|
||||
const sshConn = new SSHConnection(sshConfig);
|
||||
sshConnectionCache[connectionCacheKey] = sshConn;
|
||||
return sshConn;
|
||||
}
|
||||
|
||||
async function getSshTunnel(connection) {
|
||||
const sshConn = await getSshConnection(connection);
|
||||
const tunnelCacheKey = stableStringify(_.pick(connection, TUNNEL_FIELDS));
|
||||
if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey];
|
||||
|
||||
const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 });
|
||||
// workaround for `getPortPromise` not releasing the port quickly enough
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const tunnelConfig = {
|
||||
fromPort: localPort,
|
||||
toPort: connection.port,
|
||||
toHost: connection.server,
|
||||
};
|
||||
try {
|
||||
const tunnel = await sshConn.forward(tunnelConfig);
|
||||
console.log(
|
||||
`Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
|
||||
);
|
||||
|
||||
sshTunnelCache[tunnelCacheKey] = {
|
||||
state: 'ok',
|
||||
localPort,
|
||||
};
|
||||
return sshTunnelCache[tunnelCacheKey];
|
||||
} catch (err) {
|
||||
// error is not cached
|
||||
return {
|
||||
state: 'error',
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSshTunnel,
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const { getSshTunnel } = require('./sshTunnel');
|
||||
|
||||
const dispatchedMessages = {};
|
||||
|
||||
async function handleGetSshTunnelRequest({ msgid, connection }, subprocess) {
|
||||
const response = await getSshTunnel(connection);
|
||||
subprocess.send({ msgtype: 'getsshtunnel-response', msgid, response });
|
||||
}
|
||||
|
||||
function handleGetSshTunnelResponse({ msgid, response }, subprocess) {
|
||||
const { resolve } = dispatchedMessages[msgid];
|
||||
delete dispatchedMessages[msgid];
|
||||
resolve(response);
|
||||
}
|
||||
|
||||
async function getSshTunnelProxy(connection) {
|
||||
if (!process.send) return getSshTunnel(connection);
|
||||
const msgid = uuidv1();
|
||||
process.send({ msgtype: 'getsshtunnel-request', msgid, connection });
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatchedMessages[msgid] = { resolve, reject };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleGetSshTunnelRequest,
|
||||
handleGetSshTunnelResponse,
|
||||
getSshTunnelProxy,
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"version": "3.9.5",
|
||||
"name": "dbgate-datalib",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -12,11 +12,11 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-sqltree": "^1.0.0",
|
||||
"dbgate-filterparser": "^1.0.0"
|
||||
"dbgate-sqltree": "^3.9.5",
|
||||
"dbgate-filterparser": "^3.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"@types/node": "^13.7.0",
|
||||
"typescript": "^3.7.5"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://paypal.me/JanProchazkaCz/30eur)
|
||||
[](https://www.npmjs.com/package/dbgate)
|
||||
|
||||
# DbGate - database administration tool
|
||||
DbGate is fast and easy to use database administration tool for MySQL, PostgreSQL, SQL Server.
|
||||
|
||||
## Install using npm
|
||||
```sh
|
||||
npm install -g dbgate
|
||||
```
|
||||
|
||||
After installing, you can run dbgate with command:
|
||||
```sh
|
||||
dbgate
|
||||
```
|
||||
|
||||
Then open http://localhost:5000 in your browser
|
||||
|
||||
## Download electron app
|
||||
You can also download binary packages from https://dbgate.org . Or run from source code, as described on [github](https://github.com/dbgate/dbgate)
|
||||
|
||||
## Other dbgate packages
|
||||
You can use some functionality of dbgate from your JavaScript code. See [dbgate-api](https://npmjs.com/dbgate-api) package.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const dbgateApi = require('dbgate-api');
|
||||
|
||||
global.dbgateApiModulePath = require.resolve('dbgate-api');
|
||||
|
||||
dbgateApi.getMainModule().start('startNodeWeb');
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "3.9.5",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"description": "Opensource database administration tool - web interface",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"dbgate": "./bin/dbgate.js"
|
||||
},
|
||||
"keywords": [
|
||||
"sql",
|
||||
"dbgate",
|
||||
"web"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-api": "^3.9.5",
|
||||
"dbgate-web": "^3.9.5"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"version": "3.9.5",
|
||||
"name": "dbgate-filterparser",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -13,7 +13,7 @@
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^24.9.0",
|
||||
@@ -22,9 +22,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/parsimmon": "^1.10.1",
|
||||
"dbgate-tools": "^1.0.0",
|
||||
"dbgate-tools": "^3.9.5",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"parsimmon": "^1.13.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ console.log("Generated query:", sql);
|
||||
|
||||
```
|
||||
|
||||
See [TypeScript definitions](https://github.com/dbshell/dbgate/blob/master/packages/sqltree/src/types.ts) for complete list of available SQL command options.
|
||||
See [TypeScript definitions](https://github.com/dbgate/dbgate/blob/master/packages/sqltree/src/types.ts) for complete list of available SQL command options.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"version": "1.0.4",
|
||||
"version": "3.9.5",
|
||||
"name": "dbgate-sqltree",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "GPL",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"sql",
|
||||
"mssql",
|
||||
@@ -29,10 +29,10 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"typescript": "^3.7.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"version": "1.0.7",
|
||||
"version": "3.9.5",
|
||||
"name": "dbgate-tools",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "GPL",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"sql",
|
||||
"dbgate"
|
||||
@@ -27,7 +27,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^1.0.0",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.7.5"
|
||||
@@ -35,4 +35,4 @@
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,3 +49,15 @@ export function findObjectLike(
|
||||
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) {
|
||||
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column.columnName));
|
||||
}
|
||||
|
||||
export function makeUniqueColumnNames(res: ColumnInfo[]) {
|
||||
const usedNames = new Set();
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (usedNames.has(res[i].columnName)) {
|
||||
let suffix = 2;
|
||||
while (usedNames.has(`${res[i].columnName}${suffix}`)) suffix++;
|
||||
res[i].columnName = `${res[i].columnName}${suffix}`;
|
||||
}
|
||||
usedNames.add(res[i].columnName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"version": "1.0.2",
|
||||
"version": "3.9.5",
|
||||
"name": "dbgate-types",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "GPL",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"dbgate"
|
||||
],
|
||||
"types": "index.d.ts",
|
||||
"main": "",
|
||||
"typeScriptVersion": "2.8"
|
||||
}
|
||||
}
|
||||
+35
-33
@@ -1,8 +1,36 @@
|
||||
{
|
||||
"name": "dbgate-web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"version": "3.9.5",
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "cross-env BROWSER=none PORT=5000 react-scripts start",
|
||||
"build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
||||
"build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build",
|
||||
"build": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
||||
"prepublishOnly": "yarn build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"ts": "tsc"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/styled-components": "^4.4.2",
|
||||
"dbgate-types": "^3.9.5",
|
||||
"typescript": "^3.7.4",
|
||||
"@ant-design/colors": "^5.0.0",
|
||||
"@mdi/font": "^5.8.55",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
@@ -13,9 +41,9 @@
|
||||
"chart.js": "^2.9.4",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-datalib": "^1.0.0",
|
||||
"dbgate-sqltree": "^1.0.0",
|
||||
"dbgate-tools": "^1.0.0",
|
||||
"dbgate-datalib": "^3.9.5",
|
||||
"dbgate-sqltree": "^3.9.5",
|
||||
"dbgate-tools": "^3.9.5",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
@@ -37,31 +65,5 @@
|
||||
"sql-formatter": "^2.3.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env BROWSER=none PORT=5000 react-scripts start",
|
||||
"build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
|
||||
"build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"ts": "tsc"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/styled-components": "^4.4.2",
|
||||
"dbgate-types": "^1.0.0",
|
||||
"typescript": "^3.7.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontIcon } from './icons';
|
||||
import useTheme from './theme/useTheme';
|
||||
import getElectron from './utility/getElectron';
|
||||
import useExtensions from './utility/useExtensions';
|
||||
|
||||
const TargetStyled = styled.div`
|
||||
@@ -41,6 +42,9 @@ const TitleWrapper = styled.div`
|
||||
export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
|
||||
const theme = useTheme();
|
||||
const { fileFormats } = useExtensions();
|
||||
const electron = getElectron();
|
||||
const fileTypeNames = fileFormats.filter(x => x.readerFunc).map(x => x.name);
|
||||
if (electron) fileTypeNames.push('SQL');
|
||||
return (
|
||||
!!isDragActive && (
|
||||
<TargetStyled theme={theme}>
|
||||
@@ -49,13 +53,7 @@ export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
|
||||
<FontIcon icon="icon cloud-upload" />
|
||||
</IconWrapper>
|
||||
<TitleWrapper>Drop the files to upload to DbGate</TitleWrapper>
|
||||
<InfoWrapper>
|
||||
Supported file types:{' '}
|
||||
{fileFormats
|
||||
.filter(x => x.readerFunc)
|
||||
.map(x => x.name)
|
||||
.join(', ')}
|
||||
</InfoWrapper>
|
||||
<InfoWrapper>Supported file types: {fileTypeNames.join(', ')}</InfoWrapper>
|
||||
</InfoBox>
|
||||
<input {...inputProps} />
|
||||
</TargetStyled>
|
||||
|
||||
@@ -100,6 +100,7 @@ export default function Screen() {
|
||||
? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness
|
||||
: dimensions.widgetMenu.iconSize;
|
||||
const toolbarPortalRef = React.useRef();
|
||||
const statusbarPortalRef = React.useRef();
|
||||
const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff));
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useUploadsZone();
|
||||
@@ -131,10 +132,10 @@ export default function Screen() {
|
||||
<TabsPanel></TabsPanel>
|
||||
</TabsPanelContainer>
|
||||
<BodyDiv contentLeft={contentLeft} theme={theme}>
|
||||
<TabContent toolbarPortalRef={toolbarPortalRef} />
|
||||
<TabContent toolbarPortalRef={toolbarPortalRef} statusbarPortalRef={statusbarPortalRef} />
|
||||
</BodyDiv>
|
||||
<StausBarContainer theme={theme}>
|
||||
<StatusBar />
|
||||
<StatusBar statusbarPortalRef={statusbarPortalRef} />
|
||||
</StausBarContainer>
|
||||
<ModalLayer />
|
||||
<MenuLayer />
|
||||
|
||||
@@ -18,12 +18,18 @@ const TabContainerStyled = styled.div`
|
||||
`;
|
||||
|
||||
function TabContainer({ TabComponent, ...props }) {
|
||||
const { tabVisible, tabid, toolbarPortalRef } = props;
|
||||
const { tabVisible, tabid, toolbarPortalRef, statusbarPortalRef } = props;
|
||||
return (
|
||||
// @ts-ignore
|
||||
<TabContainerStyled tabVisible={tabVisible}>
|
||||
<ErrorBoundary>
|
||||
<TabComponent {...props} tabid={tabid} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} />
|
||||
<TabComponent
|
||||
{...props}
|
||||
tabid={tabid}
|
||||
tabVisible={tabVisible}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
statusbarPortalRef={statusbarPortalRef}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</TabContainerStyled>
|
||||
);
|
||||
@@ -42,7 +48,7 @@ function createTabComponent(selectedTab) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function TabContent({ toolbarPortalRef }) {
|
||||
export default function TabContent({ toolbarPortalRef, statusbarPortalRef }) {
|
||||
const files = useOpenedTabs();
|
||||
|
||||
const [mountedTabs, setMountedTabs] = React.useState({});
|
||||
@@ -84,6 +90,7 @@ export default function TabContent({ toolbarPortalRef }) {
|
||||
tabid={tabid}
|
||||
tabVisible={tabVisible}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
statusbarPortalRef={statusbarPortalRef}
|
||||
TabComponent={TabComponent}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import useTheme from './theme/useTheme';
|
||||
import usePropsCompare from './utility/usePropsCompare';
|
||||
import { useShowMenu } from './modals/showMenu';
|
||||
import { setSelectedTabFunc } from './utility/common';
|
||||
import getElectron from './utility/getElectron';
|
||||
|
||||
// const files = [
|
||||
// { name: 'app.js' },
|
||||
@@ -124,6 +125,15 @@ function getDbIcon(key) {
|
||||
return 'icon file';
|
||||
}
|
||||
|
||||
function buildTooltip(tab) {
|
||||
let res = tab.tooltip;
|
||||
if (tab.props && tab.props.savedFilePath) {
|
||||
if (res) res += '\n';
|
||||
res += tab.props.savedFilePath;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export default function TabsPanel() {
|
||||
// const formatDbKey = (conid, database) => `${database}-${conid}`;
|
||||
const theme = useTheme();
|
||||
@@ -210,6 +220,16 @@ export default function TabsPanel() {
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const electron = getElectron();
|
||||
if (electron) {
|
||||
const { ipcRenderer } = electron;
|
||||
const activeTab = tabs.find(x => x.selected);
|
||||
window['dbgate_activeTabId'] = activeTab ? activeTab.tabid : null;
|
||||
ipcRenderer.send('update-menu');
|
||||
}
|
||||
}, [tabs]);
|
||||
|
||||
// console.log(
|
||||
// 't',
|
||||
// tabs.map(x => x.tooltip)
|
||||
@@ -252,7 +272,7 @@ export default function TabsPanel() {
|
||||
{_.sortBy(tabsByDb[dbKey], ['title', 'tabid']).map(tab => (
|
||||
<FileTabItem
|
||||
{...tab}
|
||||
title={tab.tooltip}
|
||||
title={buildTooltip(tab)}
|
||||
key={tab.tabid}
|
||||
theme={theme}
|
||||
onClick={e => handleTabClick(e, tab.tabid)}
|
||||
|
||||
@@ -49,6 +49,7 @@ export function AppObjectCore({
|
||||
extInfo = undefined,
|
||||
statusTitle = undefined,
|
||||
disableHover = false,
|
||||
children = null,
|
||||
Menu = undefined,
|
||||
...other
|
||||
}) {
|
||||
@@ -63,31 +64,34 @@ export function AppObjectCore({
|
||||
};
|
||||
|
||||
return (
|
||||
<AppObjectDiv
|
||||
onContextMenu={handleContextMenu}
|
||||
onClick={() => {
|
||||
if (onClick) onClick(data);
|
||||
if (onClick2) onClick2(data);
|
||||
if (onClick3) onClick3(data);
|
||||
}}
|
||||
theme={theme}
|
||||
isBold={isBold}
|
||||
draggable
|
||||
onDragStart={e => {
|
||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
||||
}}
|
||||
disableHover={disableHover}
|
||||
{...other}
|
||||
>
|
||||
{prefix}
|
||||
<IconWrap>{isBusy ? <FontIcon icon="icon loading" /> : <FontIcon icon={icon} />}</IconWrap>
|
||||
{title}
|
||||
{statusIcon && (
|
||||
<StatusIconWrap>
|
||||
<FontIcon icon={statusIcon} title={statusTitle} />
|
||||
</StatusIconWrap>
|
||||
)}
|
||||
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
|
||||
</AppObjectDiv>
|
||||
<>
|
||||
<AppObjectDiv
|
||||
onContextMenu={handleContextMenu}
|
||||
onClick={() => {
|
||||
if (onClick) onClick(data);
|
||||
if (onClick2) onClick2(data);
|
||||
if (onClick3) onClick3(data);
|
||||
}}
|
||||
theme={theme}
|
||||
isBold={isBold}
|
||||
draggable
|
||||
onDragStart={e => {
|
||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
||||
}}
|
||||
disableHover={disableHover}
|
||||
{...other}
|
||||
>
|
||||
{prefix}
|
||||
<IconWrap>{isBusy ? <FontIcon icon="icon loading" /> : <FontIcon icon={icon} />}</IconWrap>
|
||||
{title}
|
||||
{statusIcon && (
|
||||
<StatusIconWrap>
|
||||
<FontIcon icon={statusIcon} title={statusTitle} />
|
||||
</StatusIconWrap>
|
||||
)}
|
||||
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
|
||||
</AppObjectDiv>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@ import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import { setSelectedTabFunc } from '../utility/common';
|
||||
import styled from 'styled-components';
|
||||
import { FontIcon } from '../icons';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const InfoDiv = styled.div`
|
||||
margin-left: 30px;
|
||||
color: ${props => props.theme.left_font3};
|
||||
`;
|
||||
|
||||
function Menu({ data }) {
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
@@ -25,17 +33,16 @@ function Menu({ data }) {
|
||||
function ClosedTabAppObject({ data, commonProps }) {
|
||||
const { tabid, props, selected, icon, title, closedTime, busy } = data;
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const theme = useTheme();
|
||||
|
||||
const onClick = () => {
|
||||
setOpenedTabs(files =>
|
||||
setSelectedTabFunc(
|
||||
files.map(
|
||||
x => ({
|
||||
...x,
|
||||
closedTime: x.tabid == tabid ? undefined : x.closedTime,
|
||||
}),
|
||||
tabid
|
||||
)
|
||||
files.map(x => ({
|
||||
...x,
|
||||
closedTime: x.tabid == tabid ? undefined : x.closedTime,
|
||||
})),
|
||||
tabid
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -50,7 +57,14 @@ function ClosedTabAppObject({ data, commonProps }) {
|
||||
onClick={onClick}
|
||||
isBusy={busy}
|
||||
Menu={Menu}
|
||||
/>
|
||||
>
|
||||
{data.props && data.props.database && (
|
||||
<InfoDiv theme={theme}>
|
||||
<FontIcon icon="icon database" /> {data.props.database}
|
||||
</InfoDiv>
|
||||
)}
|
||||
{data.contentPreview && <InfoDiv theme={theme}>{data.contentPreview}</InfoDiv>}
|
||||
</AppObjectCore>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ function Menu({ data }) {
|
||||
setOpenedConnections(list => list.filter(x => x != data._id));
|
||||
};
|
||||
const handleConnect = () => {
|
||||
setOpenedConnections(list => [...list, data._id]);
|
||||
setOpenedConnections(list => _.uniq([...list, data._id]));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
@@ -72,7 +72,7 @@ function ConnectionAppObject({ data, commonProps }) {
|
||||
const extensions = useExtensions();
|
||||
|
||||
const isBold = _.get(currentDatabase, 'connection._id') == _id;
|
||||
const onClick = () => setOpenedConnections(c => [...c, _id]);
|
||||
const onClick = () => setOpenedConnections(c => _.uniq([...c, _id]));
|
||||
|
||||
let statusIcon = null;
|
||||
let statusTitle = null;
|
||||
|
||||
@@ -20,7 +20,7 @@ function Menu({ data }) {
|
||||
|
||||
const handleNewQuery = () => {
|
||||
openNewTab({
|
||||
title: 'Query',
|
||||
title: 'Query #',
|
||||
icon: 'img sql-file',
|
||||
tooltip,
|
||||
tabComponent: 'QueryTab',
|
||||
|
||||
@@ -149,7 +149,7 @@ export async function openDatabaseObjectDetail(
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: pureName,
|
||||
title: sqlTemplate ? 'Query #' : pureName,
|
||||
tooltip,
|
||||
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
|
||||
tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
|
||||
@@ -245,7 +245,7 @@ function Menu({ data }) {
|
||||
} else if (menu.isQueryDesigner) {
|
||||
openNewTab(
|
||||
{
|
||||
title: data.pureName,
|
||||
title: 'Query #',
|
||||
icon: 'img query-design',
|
||||
tabComponent: 'QueryDesignTab',
|
||||
props: {
|
||||
|
||||
@@ -104,7 +104,7 @@ export function SavedSqlFileAppObject({ data, commonProps }) {
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Shell',
|
||||
title: 'Shell #',
|
||||
icon: 'img shell',
|
||||
tabComponent: 'ShellTab',
|
||||
},
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import React from 'react';
|
||||
import useHasPermission from '../utility/useHasPermission';
|
||||
import ToolbarButton from '../widgets/ToolbarButton';
|
||||
|
||||
export default function ChartToolbar({ save, modelState, dispatchModel }) {
|
||||
const hasPermission = useHasPermission();
|
||||
|
||||
export default function ChartToolbar({ modelState, dispatchModel }) {
|
||||
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>
|
||||
|
||||
@@ -312,7 +312,7 @@ export default function DataGridCore(props) {
|
||||
}
|
||||
}
|
||||
: null,
|
||||
[formViewAvailable, display]
|
||||
[formViewAvailable, display, openNewTab]
|
||||
);
|
||||
|
||||
if (!columns || columns.length == 0) return <LoadingInfo wrapper message="Waiting for structure" />;
|
||||
@@ -353,7 +353,7 @@ export default function DataGridCore(props) {
|
||||
const handleOpenFreeTable = () => {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'selection',
|
||||
title: 'Data #',
|
||||
icon: 'img free-table',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {},
|
||||
@@ -365,7 +365,7 @@ export default function DataGridCore(props) {
|
||||
const handleOpenChart = () => {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Chart',
|
||||
title: 'Chart #',
|
||||
icon: 'img chart',
|
||||
tabComponent: 'ChartTab',
|
||||
props: {},
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function SqlDataGridCore(props) {
|
||||
function openActiveChart() {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Chart',
|
||||
title: 'Chart #',
|
||||
icon: 'img chart',
|
||||
tabComponent: 'ChartTab',
|
||||
props: {
|
||||
@@ -104,7 +104,7 @@ export default function SqlDataGridCore(props) {
|
||||
function openQuery() {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Query',
|
||||
title: 'Query #',
|
||||
icon: 'img sql-file',
|
||||
tabComponent: 'QueryTab',
|
||||
props: {
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import React from 'react';
|
||||
import useHasPermission from '../utility/useHasPermission';
|
||||
import ToolbarButton from '../widgets/ToolbarButton';
|
||||
|
||||
export default function QueryDesignToolbar({
|
||||
execute,
|
||||
isDatabaseDefined,
|
||||
busy,
|
||||
save,
|
||||
modelState,
|
||||
dispatchModel,
|
||||
isConnected,
|
||||
kill,
|
||||
}) {
|
||||
const hasPermission = useHasPermission();
|
||||
return (
|
||||
<>
|
||||
<ToolbarButton disabled={!isDatabaseDefined || busy} onClick={execute} icon="icon run">
|
||||
@@ -21,11 +18,6 @@ export default function QueryDesignToolbar({
|
||||
<ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close">
|
||||
Kill
|
||||
</ToolbarButton>
|
||||
{hasPermission('files/query/write') && (
|
||||
<ToolbarButton onClick={save} icon="icon save">
|
||||
Save
|
||||
</ToolbarButton>
|
||||
)}
|
||||
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
|
||||
Undo
|
||||
</ToolbarButton>
|
||||
|
||||
@@ -15,7 +15,7 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #ddeeee;
|
||||
background: ${props => props.theme.gridheader_background_cyan[0]};
|
||||
height: ${dimensions.toolBar.height}px;
|
||||
min-height: ${dimensions.toolBar.height}px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function useNewFreeTable() {
|
||||
|
||||
return ({ title = undefined, ...props } = {}) =>
|
||||
openNewTab({
|
||||
title: title || 'Table',
|
||||
title: title || 'Data #',
|
||||
icon: 'img free-table',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props,
|
||||
|
||||
@@ -411,7 +411,11 @@ function SourceName({ name }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function ImportExportConfigurator({ uploadedFile = undefined, onChangePreview = undefined }) {
|
||||
export default function ImportExportConfigurator({
|
||||
uploadedFile = undefined,
|
||||
openedFile = undefined,
|
||||
onChangePreview = undefined,
|
||||
}) {
|
||||
const { values, setFieldValue, setValues } = useForm();
|
||||
const targetDbinfo = useDatabaseInfo({ conid: values.targetConnectionId, database: values.targetDatabaseName });
|
||||
const sourceConnectionInfo = useConnectionInfo({ conid: values.sourceConnectionId });
|
||||
@@ -453,6 +457,21 @@ export default function ImportExportConfigurator({ uploadedFile = undefined, onC
|
||||
if (uploadedFile) {
|
||||
handleUpload(uploadedFile);
|
||||
}
|
||||
if (openedFile) {
|
||||
addFilesToSourceList(
|
||||
extensions,
|
||||
[
|
||||
{
|
||||
fileName: openedFile.filePath,
|
||||
shortName: openedFile.shortName,
|
||||
},
|
||||
],
|
||||
values,
|
||||
setValues,
|
||||
!sourceList || sourceList.length == 0 ? openedFile.storageType : null,
|
||||
setPreviewSource
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const supportsPreview =
|
||||
|
||||
@@ -31,7 +31,7 @@ async function getConnection(extensions, storageType, conid, database) {
|
||||
const driver = findEngineDriver(conn, extensions);
|
||||
return [
|
||||
{
|
||||
..._.pick(conn, ['server', 'engine', 'user', 'password', 'port', 'authType']),
|
||||
..._.omit(conn, ['_id', 'displayName']),
|
||||
database,
|
||||
},
|
||||
driver,
|
||||
|
||||
@@ -26,3 +26,27 @@ code {
|
||||
.icon-invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.largeFormMarker input[type='text'] {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.largeFormMarker input[type='password'] {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.largeFormMarker select {
|
||||
width: 100%;
|
||||
padding: 10px 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import _ from 'lodash';
|
||||
import './index.css';
|
||||
import '@mdi/font/css/materialdesignicons.css';
|
||||
import App from './App';
|
||||
@@ -22,6 +23,17 @@ import localStorageGarbageCollector from './utility/localStorageGarbageCollector
|
||||
// import 'ace-builds/src-noconflict/snippets/mysql';
|
||||
|
||||
localStorageGarbageCollector();
|
||||
window['dbgate_tabExports'] = {};
|
||||
window['dbgate_getCurrentTabCommands'] = () => {
|
||||
const tabid = window['dbgate_activeTabId'];
|
||||
return _.mapValues(window['dbgate_tabExports'][tabid] || {}, v => !!v);
|
||||
};
|
||||
window['dbgate_tabCommand'] = cmd => {
|
||||
const tabid = window['dbgate_activeTabId'];
|
||||
const commands = window['dbgate_tabExports'][tabid];
|
||||
const func = (commands || {})[cmd];
|
||||
if (func) func();
|
||||
};
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import React from 'react';
|
||||
import useHasPermission from '../utility/useHasPermission';
|
||||
import ToolbarButton from '../widgets/ToolbarButton';
|
||||
|
||||
export default function MarkdownToolbar({ save, showPreview }) {
|
||||
const hasPermission = useHasPermission();
|
||||
|
||||
export default function MarkdownToolbar({ showPreview }) {
|
||||
return (
|
||||
<>
|
||||
{hasPermission('files/markdown/write') && (
|
||||
<ToolbarButton onClick={save} icon="icon save">
|
||||
Save
|
||||
</ToolbarButton>
|
||||
)}
|
||||
<ToolbarButton onClick={showPreview} icon="icon preview">
|
||||
Preview
|
||||
</ToolbarButton>
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function AboutModal({ modalState }) {
|
||||
<Link label="Web" href="https://dbgate.org">
|
||||
dbgate.org
|
||||
</Link>
|
||||
<Link label="Source codes" href="https://github.com/dbshell/dbgate/">
|
||||
<Link label="Source codes" href="https://github.com/dbgate/dbgate/">
|
||||
github
|
||||
</Link>
|
||||
<Link label="Docker container" href="https://hub.docker.com/r/dbgate/dbgate">
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import React from 'react';
|
||||
import axios from '../utility/axios';
|
||||
import ModalBase from './ModalBase';
|
||||
import { FormButton, FormTextField, FormSelectField, FormSubmit, FormPasswordField } from '../utility/forms';
|
||||
import {
|
||||
FormButton,
|
||||
FormTextField,
|
||||
FormSelectField,
|
||||
FormSubmit,
|
||||
FormPasswordField,
|
||||
FormCheckboxField,
|
||||
FormElectronFileSelector,
|
||||
} from '../utility/forms';
|
||||
import ModalHeader from './ModalHeader';
|
||||
import ModalFooter from './ModalFooter';
|
||||
import ModalContent from './ModalContent';
|
||||
@@ -9,8 +17,35 @@ import useExtensions from '../utility/useExtensions';
|
||||
import LoadingInfo from '../widgets/LoadingInfo';
|
||||
import { FontIcon } from '../icons';
|
||||
import { FormProvider, useForm } from '../utility/FormProvider';
|
||||
import { TabControl, TabPage } from '../widgets/TabControl';
|
||||
import { usePlatformInfo } from '../utility/metadataLoaders';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { FormFieldTemplateLarge, FormRowLarge } from '../utility/formStyle';
|
||||
import styled from 'styled-components';
|
||||
import { FlexCol3, FlexCol6, FlexCol9 } from '../utility/flexGrid';
|
||||
// import FormikForm from '../utility/FormikForm';
|
||||
|
||||
const FlexContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const TestResultContainer = styled.div`
|
||||
margin-left: 10px;
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const ButtonsContainer = styled.div`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const AgentInfoWrap = styled.div`
|
||||
margin-left: 20px;
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
function DriverFields({ extensions }) {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const { authType, engine } = values;
|
||||
@@ -46,10 +81,50 @@ function DriverFields({ extensions }) {
|
||||
))}
|
||||
</FormSelectField>
|
||||
)}
|
||||
<FormTextField label="Server" name="server" disabled={disabledFields.includes('server')} />
|
||||
<FormTextField label="Port" name="port" disabled={disabledFields.includes('port')} />
|
||||
<FormTextField label="User" name="user" disabled={disabledFields.includes('user')} />
|
||||
<FormPasswordField label="Password" name="password" disabled={disabledFields.includes('password')} />
|
||||
<FormRowLarge>
|
||||
<FlexCol9
|
||||
//@ts-ignore
|
||||
marginRight={5}
|
||||
>
|
||||
<FormTextField
|
||||
label="Server"
|
||||
name="server"
|
||||
disabled={disabledFields.includes('server')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</FlexCol9>
|
||||
<FlexCol3>
|
||||
<FormTextField
|
||||
label="Port"
|
||||
name="port"
|
||||
disabled={disabledFields.includes('port')}
|
||||
templateProps={{ noMargin: true }}
|
||||
placeholder={driver && driver.defaultPort}
|
||||
/>
|
||||
</FlexCol3>
|
||||
</FormRowLarge>
|
||||
<FormRowLarge>
|
||||
<FlexCol6
|
||||
//@ts-ignore
|
||||
marginRight={5}
|
||||
>
|
||||
<FormTextField
|
||||
label="User"
|
||||
name="user"
|
||||
disabled={disabledFields.includes('user')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</FlexCol6>
|
||||
<FlexCol6>
|
||||
<FormPasswordField
|
||||
label="Password"
|
||||
name="password"
|
||||
disabled={disabledFields.includes('password')}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</FlexCol6>
|
||||
</FormRowLarge>
|
||||
|
||||
{!disabledFields.includes('password') && (
|
||||
<FormSelectField label="Password mode" name="passwordMode">
|
||||
<option value="saveEncrypted">Save and encrypt</option>
|
||||
@@ -60,6 +135,124 @@ function DriverFields({ extensions }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SshTunnelFields() {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const { useSshTunnel, sshMode, sshPort, sshKeyfile } = values;
|
||||
const platformInfo = usePlatformInfo();
|
||||
const electron = getElectron();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (useSshTunnel && !sshMode) {
|
||||
setFieldValue('sshMode', 'userPassword');
|
||||
}
|
||||
if (useSshTunnel && !sshPort) {
|
||||
setFieldValue('sshPort', '22');
|
||||
}
|
||||
if (useSshTunnel && sshMode == 'keyFile' && !sshKeyfile) {
|
||||
setFieldValue('sshKeyfile', platformInfo.defaultKeyFile);
|
||||
}
|
||||
}, [useSshTunnel, sshMode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormCheckboxField label="Use SSH tunnel" name="useSshTunnel" />
|
||||
<FormRowLarge>
|
||||
<FlexCol9
|
||||
//@ts-ignore
|
||||
marginRight={5}
|
||||
>
|
||||
<FormTextField label="Host" name="sshHost" disabled={!useSshTunnel} templateProps={{ noMargin: true }} />
|
||||
</FlexCol9>
|
||||
<FlexCol3>
|
||||
<FormTextField label="Port" name="sshPort" disabled={!useSshTunnel} templateProps={{ noMargin: true }} />
|
||||
</FlexCol3>
|
||||
</FormRowLarge>
|
||||
<FormTextField label="Bastion host (Jump host)" name="sshBastionHost" disabled={!useSshTunnel} />
|
||||
|
||||
<FormSelectField label="SSH Authentication" name="sshMode" disabled={!useSshTunnel}>
|
||||
<option value="userPassword">Username & password</option>
|
||||
<option value="agent">SSH agent</option>
|
||||
{!!electron && <option value="keyFile">Key file</option>}
|
||||
</FormSelectField>
|
||||
|
||||
{sshMode != 'userPassword' && <FormTextField label="Login" name="sshLogin" disabled={!useSshTunnel} />}
|
||||
|
||||
{sshMode == 'userPassword' && (
|
||||
<FormRowLarge>
|
||||
<FlexCol6
|
||||
//@ts-ignore
|
||||
marginRight={5}
|
||||
>
|
||||
<FormTextField label="Login" name="sshLogin" disabled={!useSshTunnel} templateProps={{ noMargin: true }} />
|
||||
</FlexCol6>
|
||||
<FlexCol6>
|
||||
<FormPasswordField
|
||||
label="Password"
|
||||
name="sshPassword"
|
||||
disabled={!useSshTunnel}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</FlexCol6>
|
||||
</FormRowLarge>
|
||||
)}
|
||||
|
||||
{sshMode == 'keyFile' && (
|
||||
<FormRowLarge>
|
||||
<FlexCol6
|
||||
//@ts-ignore
|
||||
marginRight={5}
|
||||
>
|
||||
<FormElectronFileSelector
|
||||
label="Private key file"
|
||||
name="sshKeyfile"
|
||||
disabled={!useSshTunnel}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</FlexCol6>
|
||||
<FlexCol6>
|
||||
<FormPasswordField
|
||||
label="Key file passphrase"
|
||||
name="sshKeyfilePassword"
|
||||
disabled={!useSshTunnel}
|
||||
templateProps={{ noMargin: true }}
|
||||
/>
|
||||
</FlexCol6>
|
||||
</FormRowLarge>
|
||||
)}
|
||||
|
||||
{useSshTunnel && sshMode == 'agent' && (
|
||||
<AgentInfoWrap>
|
||||
{platformInfo.sshAuthSock ? (
|
||||
<div>
|
||||
<FontIcon icon="img ok" /> SSH Agent found
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<FontIcon icon="img error" /> SSH Agent not found
|
||||
</div>
|
||||
)}
|
||||
</AgentInfoWrap>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SslFields() {
|
||||
const { values } = useForm();
|
||||
const { useSsl } = values;
|
||||
const electron = getElectron();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormCheckboxField label="Use SSL" name="useSsl" />
|
||||
<FormElectronFileSelector label="CA Cert (optional)" name="sslCaFile" disabled={!useSsl || !electron} />
|
||||
<FormElectronFileSelector label="Certificate (optional)" name="sslCertFile" disabled={!useSsl || !electron} />
|
||||
<FormElectronFileSelector label="Key file (optional)" name="sslKeyFile" disabled={!useSsl || !electron} />
|
||||
<FormCheckboxField label="Reject unauthorized" name="sslRejectUnauthorized" disabled={!useSsl} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ConnectionModal({ modalState, connection = undefined }) {
|
||||
const [sqlConnectResult, setSqlConnectResult] = React.useState(null);
|
||||
const extensions = useExtensions();
|
||||
@@ -89,42 +282,66 @@ export default function ConnectionModal({ modalState, connection = undefined })
|
||||
return (
|
||||
<ModalBase modalState={modalState}>
|
||||
<ModalHeader modalState={modalState}>{connection ? 'Edit connection' : 'Add connection'}</ModalHeader>
|
||||
<FormProvider initialValues={connection || { server: 'localhost', engine: 'mssql@dbgate-plugin-mssql' }}>
|
||||
<ModalContent>
|
||||
<FormSelectField label="Database engine" name="engine">
|
||||
<option value="(select driver)"></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@dbgate-plugin-mssql' }}
|
||||
template={FormFieldTemplateLarge}
|
||||
>
|
||||
<ModalContent noPadding>
|
||||
<TabControl isInline>
|
||||
<TabPage label="Main" key="main">
|
||||
<FormSelectField label="Database engine" name="engine">
|
||||
<option value="(select driver)"></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>
|
||||
<DriverFields extensions={extensions} />
|
||||
<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" />}
|
||||
</FormSelectField>
|
||||
<DriverFields extensions={extensions} />
|
||||
<FormTextField label="Display name" name="displayName" />
|
||||
</TabPage>
|
||||
<TabPage label="SSH Tunnel" key="sshTunnel">
|
||||
<SshTunnelFields />
|
||||
</TabPage>
|
||||
<TabPage label="SSL" key="ssl">
|
||||
<SslFields />
|
||||
</TabPage>
|
||||
</TabControl>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
{isTesting ? (
|
||||
<FormButton value="Cancel" onClick={handleCancel} />
|
||||
) : (
|
||||
<FormButton value="Test" onClick={handleTest} />
|
||||
)}
|
||||
<FlexContainer>
|
||||
<ButtonsContainer>
|
||||
{isTesting ? (
|
||||
<FormButton value="Cancel" onClick={handleCancel} />
|
||||
) : (
|
||||
<FormButton value="Test" onClick={handleTest} />
|
||||
)}
|
||||
|
||||
<FormSubmit value="Save" onClick={handleSubmit} />
|
||||
<FormSubmit value="Save" onClick={handleSubmit} />
|
||||
</ButtonsContainer>
|
||||
|
||||
<TestResultContainer>
|
||||
{!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 && (
|
||||
<div>
|
||||
<FontIcon icon="icon loading" /> Testing connection
|
||||
</div>
|
||||
)}
|
||||
</TestResultContainer>
|
||||
</FlexContainer>
|
||||
</ModalFooter>
|
||||
</FormProvider>
|
||||
</ModalBase>
|
||||
|
||||
@@ -100,7 +100,7 @@ function GenerateSctriptButton({ modalState }) {
|
||||
const code = await createImpExpScript(extensions, values);
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Shell',
|
||||
title: 'Shell #',
|
||||
icon: 'img shell',
|
||||
tabComponent: 'ShellTab',
|
||||
},
|
||||
@@ -120,6 +120,7 @@ export default function ImportExportModal({
|
||||
modalState,
|
||||
initialValues,
|
||||
uploadedFile = undefined,
|
||||
openedFile = undefined,
|
||||
importToArchive = false,
|
||||
}) {
|
||||
const [executeNumber, setExecuteNumber] = React.useState(0);
|
||||
@@ -195,7 +196,11 @@ export default function ImportExportModal({
|
||||
<ModalHeader modalState={modalState}>Import/Export {busy && <FontIcon icon="icon loading" />}</ModalHeader>
|
||||
<Wrapper>
|
||||
<ContentWrapper theme={theme}>
|
||||
<ImportExportConfigurator uploadedFile={uploadedFile} onChangePreview={setPreviewReader} />
|
||||
<ImportExportConfigurator
|
||||
uploadedFile={uploadedFile}
|
||||
openedFile={openedFile}
|
||||
onChangePreview={setPreviewReader}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
<WidgetColumnWrapper theme={theme}>
|
||||
<WidgetColumnBar>
|
||||
|
||||
@@ -5,10 +5,23 @@ import useTheme from '../theme/useTheme';
|
||||
const Wrapper = styled.div`
|
||||
border-bottom: 1px solid ${props => props.theme.border};
|
||||
border-top: 1px solid ${props => props.theme.border};
|
||||
${props =>
|
||||
// @ts-ignore
|
||||
!props.noPadding &&
|
||||
`
|
||||
padding: 15px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default function ModalContent({ children }) {
|
||||
export default function ModalContent({ children, noPadding = false }) {
|
||||
const theme = useTheme();
|
||||
return <Wrapper theme={theme}>{children}</Wrapper>;
|
||||
return (
|
||||
<Wrapper
|
||||
theme={theme}
|
||||
// @ts-ignore
|
||||
noPadding={noPadding}
|
||||
>
|
||||
{children}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,51 @@ import ModalHeader from './ModalHeader';
|
||||
import ModalContent from './ModalContent';
|
||||
import ModalFooter from './ModalFooter';
|
||||
import { FormProvider } from '../utility/FormProvider';
|
||||
import FormStyledButton from '../widgets/FormStyledButton';
|
||||
import getElectron from '../utility/getElectron';
|
||||
|
||||
export default function SaveFileModal({
|
||||
data,
|
||||
folder,
|
||||
format,
|
||||
modalState,
|
||||
name,
|
||||
fileExtension,
|
||||
filePath,
|
||||
onSave = undefined,
|
||||
}) {
|
||||
const electron = getElectron();
|
||||
|
||||
export default function SaveFileModal({ data, folder, format, modalState, name, onSave = undefined }) {
|
||||
const handleSubmit = async values => {
|
||||
const { name } = values;
|
||||
await axios.post('files/save', { folder, file: name, data, format });
|
||||
modalState.close();
|
||||
if (onSave) onSave(name);
|
||||
if (onSave) {
|
||||
onSave(name, {
|
||||
savedFile: name,
|
||||
savedFolder: folder,
|
||||
savedFilePath: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveToDisk = async filePath => {
|
||||
const path = window.require('path');
|
||||
const parsed = path.parse(filePath);
|
||||
// if (!parsed.ext) filePath += `.${fileExtension}`;
|
||||
|
||||
await axios.post('files/save-as', { filePath, data, format });
|
||||
modalState.close();
|
||||
|
||||
if (onSave) {
|
||||
onSave(parsed.name, {
|
||||
savedFile: null,
|
||||
savedFolder: null,
|
||||
savedFilePath: filePath,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalBase modalState={modalState}>
|
||||
<ModalHeader modalState={modalState}>Save file</ModalHeader>
|
||||
@@ -23,6 +60,25 @@ export default function SaveFileModal({ data, folder, format, modalState, name,
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<FormSubmit value="Save" onClick={handleSubmit} />
|
||||
{electron && (
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
value="Save to disk"
|
||||
onClick={() => {
|
||||
const file = electron.remote.dialog.showSaveDialogSync(electron.remote.getCurrentWindow(), {
|
||||
filters: [
|
||||
{ name: `${fileExtension.toUpperCase()} files`, extensions: [fileExtension] },
|
||||
{ name: `All files`, extensions: ['*'] },
|
||||
],
|
||||
defaultPath: filePath || `${name}.${fileExtension}`,
|
||||
properties: ['showOverwriteConfirmation'],
|
||||
});
|
||||
if (file) {
|
||||
handleSaveToDisk(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</FormProvider>
|
||||
</ModalBase>
|
||||
|
||||
@@ -1,53 +1,117 @@
|
||||
import React from 'react';
|
||||
import axios from '../utility/axios';
|
||||
import { changeTab } from '../utility/common';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import SaveFileToolbarButton from '../utility/SaveFileToolbarButton';
|
||||
import ToolbarPortal from '../utility/ToolbarPortal';
|
||||
import useHasPermission from '../utility/useHasPermission';
|
||||
import SaveFileModal from './SaveFileModal';
|
||||
import useModalState from './useModalState';
|
||||
|
||||
export default function SaveTabModal({ data, folder, format, modalState, tabid, tabVisible }) {
|
||||
export default function SaveTabModal({
|
||||
data,
|
||||
folder,
|
||||
format,
|
||||
tabid,
|
||||
tabVisible,
|
||||
fileExtension,
|
||||
toolbarPortalRef = undefined,
|
||||
}) {
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const openedTabs = useOpenedTabs();
|
||||
const saveFileModalState = useModalState();
|
||||
const hasPermission = useHasPermission();
|
||||
const canSave = hasPermission(`files/${folder}/write`);
|
||||
|
||||
const { savedFile } = openedTabs.find(x => x.tabid == tabid).props || {};
|
||||
const onSave = name =>
|
||||
const { savedFile, savedFilePath } = openedTabs.find(x => x.tabid == tabid).props || {};
|
||||
const onSave = (title, newProps) => {
|
||||
changeTab(tabid, setOpenedTabs, tab => ({
|
||||
...tab,
|
||||
title: name,
|
||||
title,
|
||||
props: {
|
||||
...tab.props,
|
||||
savedFile: name,
|
||||
savedFolder: folder,
|
||||
savedFormat: format,
|
||||
...newProps,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (savedFile) {
|
||||
await axios.post('files/save', { folder, file: savedFile, data, format });
|
||||
}
|
||||
if (savedFilePath) {
|
||||
await axios.post('files/save-as', { filePath: savedFilePath, data, format });
|
||||
}
|
||||
};
|
||||
const handleSaveRef = React.useRef(handleSave);
|
||||
handleSaveRef.current = handleSave;
|
||||
|
||||
const handleKeyboard = React.useCallback(
|
||||
e => {
|
||||
if (e.keyCode == keycodes.s && e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
modalState.open();
|
||||
if (e.shiftKey) {
|
||||
saveFileModalState.open();
|
||||
} else {
|
||||
if (savedFile || savedFilePath) handleSaveRef.current();
|
||||
else saveFileModalState.open();
|
||||
}
|
||||
}
|
||||
},
|
||||
[modalState]
|
||||
[saveFileModalState]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tabVisible) {
|
||||
if (tabVisible && canSave) {
|
||||
document.addEventListener('keydown', handleKeyboard);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyboard);
|
||||
};
|
||||
}
|
||||
}, [tabVisible, handleKeyboard]);
|
||||
}, [tabVisible, handleKeyboard, canSave]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const electron = getElectron();
|
||||
if (electron) {
|
||||
const { ipcRenderer } = electron;
|
||||
window['dbgate_tabExports'][tabid] = {
|
||||
save: handleSaveRef.current,
|
||||
saveAs: saveFileModalState.open,
|
||||
};
|
||||
ipcRenderer.send('update-menu');
|
||||
|
||||
return () => {
|
||||
delete window['dbgate_tabExports'][tabid];
|
||||
ipcRenderer.send('update-menu');
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SaveFileModal
|
||||
data={data}
|
||||
folder={folder}
|
||||
format={format}
|
||||
modalState={modalState}
|
||||
name={savedFile || 'newFile'}
|
||||
onSave={onSave}
|
||||
/>
|
||||
<>
|
||||
<SaveFileModal
|
||||
data={data}
|
||||
folder={folder}
|
||||
format={format}
|
||||
modalState={saveFileModalState}
|
||||
name={savedFile || 'newFile'}
|
||||
filePath={savedFilePath}
|
||||
fileExtension={fileExtension}
|
||||
onSave={onSave}
|
||||
/>
|
||||
|
||||
{canSave && (
|
||||
<ToolbarPortal tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef}>
|
||||
<SaveFileToolbarButton
|
||||
saveAs={saveFileModalState.open}
|
||||
save={savedFile || savedFilePath ? handleSave : null}
|
||||
tabid={tabid}
|
||||
/>
|
||||
</ToolbarPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import useHasPermission from '../utility/useHasPermission';
|
||||
import ToolbarButton from '../widgets/ToolbarButton';
|
||||
|
||||
export default function QueryToolbar({ execute, isDatabaseDefined, busy, save, format, isConnected, kill }) {
|
||||
export default function QueryToolbar({ execute, isDatabaseDefined, busy, format, isConnected, kill }) {
|
||||
const hasPermission = useHasPermission();
|
||||
return (
|
||||
<>
|
||||
@@ -15,11 +15,11 @@ export default function QueryToolbar({ execute, isDatabaseDefined, busy, save, f
|
||||
<ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close">
|
||||
Kill
|
||||
</ToolbarButton>
|
||||
{hasPermission('files/sql/write') && (
|
||||
{/* {hasPermission('files/sql/write') && (
|
||||
<ToolbarButton onClick={save} icon="icon save">
|
||||
Save
|
||||
</ToolbarButton>
|
||||
)}
|
||||
)} */}
|
||||
<ToolbarButton onClick={format} icon="icon format-code">
|
||||
Format
|
||||
</ToolbarButton>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import useHasPermission from '../utility/useHasPermission';
|
||||
import ToolbarButton from '../widgets/ToolbarButton';
|
||||
|
||||
export default function ShellToolbar({ execute, cancel, busy, edit, save, editAvailable }) {
|
||||
const hasPermission = useHasPermission();
|
||||
export default function ShellToolbar({ execute, cancel, busy, edit, editAvailable }) {
|
||||
return (
|
||||
<>
|
||||
<ToolbarButton disabled={busy} onClick={execute} icon="icon run">
|
||||
@@ -15,11 +13,6 @@ export default function ShellToolbar({ execute, cancel, busy, edit, save, editAv
|
||||
<ToolbarButton disabled={!editAvailable} onClick={edit} icon="icon show-wizard">
|
||||
Show wizard
|
||||
</ToolbarButton>
|
||||
{hasPermission('files/shell/write') && (
|
||||
<ToolbarButton onClick={save} icon="icon save">
|
||||
Save
|
||||
</ToolbarButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function useNewQuery() {
|
||||
return ({ title = undefined, initialData = undefined, ...props } = {}) =>
|
||||
openNewTab(
|
||||
{
|
||||
title: title || 'Query',
|
||||
title: title || 'Query #',
|
||||
icon: 'img sql-file',
|
||||
tooltip,
|
||||
tabComponent: 'QueryTab',
|
||||
@@ -40,7 +40,7 @@ export function useNewQueryDesign() {
|
||||
return ({ title = undefined, initialData = undefined, ...props } = {}) =>
|
||||
openNewTab(
|
||||
{
|
||||
title: title || 'Query',
|
||||
title: title || 'Query #',
|
||||
icon: 'img query-design',
|
||||
tooltip,
|
||||
tabComponent: 'QueryDesignTab',
|
||||
|
||||
@@ -9,6 +9,9 @@ import useShowModal from '../modals/showModal';
|
||||
import InsertJoinModal from '../modals/InsertJoinModal';
|
||||
import { getDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import { useShowMenu } from '../modals/showMenu';
|
||||
import SqlEditorContextMenu from './SqlEditorContextMenu';
|
||||
import sqlFormatter from 'sql-formatter';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: absolute;
|
||||
@@ -53,10 +56,12 @@ export default function SqlEditor({
|
||||
focusOnCreate = false,
|
||||
conid = undefined,
|
||||
database = undefined,
|
||||
onExecute = undefined,
|
||||
}) {
|
||||
const [containerRef, { height, width }] = useDimensions();
|
||||
const ownEditorRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
const showMenu = useShowMenu();
|
||||
|
||||
const currentEditorRef = editorRef || ownEditorRef;
|
||||
const showModal = useShowModal();
|
||||
@@ -73,23 +78,27 @@ export default function SqlEditor({
|
||||
currentEditorRef.current.editor.focus();
|
||||
}, [tabVisible, focusOnCreate]);
|
||||
|
||||
const handleInsertJoin = async () => {
|
||||
const dbinfo = await getDatabaseInfo({ conid, database });
|
||||
showModal(modalState => (
|
||||
<InsertJoinModal
|
||||
sql={currentEditorRef.current.editor.getValue()}
|
||||
modalState={modalState}
|
||||
engine={engine}
|
||||
dbinfo={dbinfo}
|
||||
onInsert={text => {
|
||||
const editor = currentEditorRef.current.editor;
|
||||
editor.session.insert(editor.getCursorPosition(), text);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
async (data, hash, keyString, keyCode, event) => {
|
||||
(data, hash, keyString, keyCode, event) => {
|
||||
if (keyCode == keycodes.j && event.ctrlKey && !readOnly && tabVisible) {
|
||||
event.preventDefault();
|
||||
const dbinfo = await getDatabaseInfo({ conid, database });
|
||||
showModal(modalState => (
|
||||
<InsertJoinModal
|
||||
sql={currentEditorRef.current.editor.getValue()}
|
||||
modalState={modalState}
|
||||
engine={engine}
|
||||
dbinfo={dbinfo}
|
||||
onInsert={text => {
|
||||
const editor = currentEditorRef.current.editor;
|
||||
editor.session.insert(editor.getCursorPosition(), text);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
handleInsertJoin();
|
||||
}
|
||||
|
||||
if (onKeyDown) onKeyDown(data, hash, keyString, keyCode, event);
|
||||
@@ -100,12 +109,40 @@ export default function SqlEditor({
|
||||
React.useEffect(() => {
|
||||
if ((onKeyDown || !readOnly) && currentEditorRef.current) {
|
||||
currentEditorRef.current.editor.keyBinding.addKeyboardHandler(handleKeyDown);
|
||||
|
||||
return () => {
|
||||
currentEditorRef.current.editor.keyBinding.removeKeyboardHandler(handleKeyDown);
|
||||
};
|
||||
}
|
||||
return () => {
|
||||
currentEditorRef.current.editor.keyBinding.removeKeyboardHandler(handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
const handleFormatCode = () => {
|
||||
currentEditorRef.current.editor.setValue(sqlFormatter.format(editorRef.current.editor.getValue()));
|
||||
currentEditorRef.current.editor.clearSelection();
|
||||
};
|
||||
|
||||
const menuRefs = React.useRef(null);
|
||||
menuRefs.current = {
|
||||
execute: onExecute,
|
||||
insertJoin: !readOnly ? handleInsertJoin : null,
|
||||
toggleComment: !readOnly ? () => currentEditorRef.current.editor.execCommand('togglecomment') : null,
|
||||
formatCode: !readOnly ? handleFormatCode : null,
|
||||
};
|
||||
const handleContextMenu = React.useCallback(event => {
|
||||
event.preventDefault();
|
||||
showMenu(event.pageX, event.pageY, <SqlEditorContextMenu {...menuRefs.current} />);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (currentEditorRef.current) {
|
||||
currentEditorRef.current.editor.container.addEventListener('contextmenu', handleContextMenu);
|
||||
|
||||
return () => {
|
||||
currentEditorRef.current.editor.container.removeEventListener('contextmenu', handleContextMenu);
|
||||
};
|
||||
}
|
||||
}, [handleContextMenu]);
|
||||
|
||||
return (
|
||||
<Wrapper ref={containerRef}>
|
||||
<AceEditor
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
|
||||
|
||||
export default function SqlEditorContextMenu({ execute, insertJoin, toggleComment, formatCode }) {
|
||||
return (
|
||||
<>
|
||||
{!!execute && (
|
||||
<DropDownMenuItem onClick={execute} keyText="F5 or Ctrl+Enter">
|
||||
Execute query
|
||||
</DropDownMenuItem>
|
||||
)}
|
||||
{!!insertJoin && (
|
||||
<DropDownMenuItem onClick={insertJoin} keyText="Ctrl+J">
|
||||
Insert SQL Join
|
||||
</DropDownMenuItem>
|
||||
)}
|
||||
{!!toggleComment && (
|
||||
<DropDownMenuItem onClick={toggleComment} keyText="Ctrl+/">
|
||||
Toggle comment
|
||||
</DropDownMenuItem>
|
||||
)}
|
||||
{!!formatCode && (
|
||||
<DropDownMenuItem onClick={formatCode} >
|
||||
Format code
|
||||
</DropDownMenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,17 +4,16 @@ import { createFreeTableModel } from 'dbgate-datalib';
|
||||
import useUndoReducer from '../utility/useUndoReducer';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useUpdateDatabaseForTab } from '../utility/globalState';
|
||||
import useModalState from '../modals/useModalState';
|
||||
import LoadingInfo from '../widgets/LoadingInfo';
|
||||
import ErrorInfo from '../widgets/ErrorInfo';
|
||||
import useEditorData from '../utility/useEditorData';
|
||||
import SaveTabModal from '../modals/SaveTabModal';
|
||||
import ChartEditor from '../charts/ChartEditor';
|
||||
import ChartToolbar from '../charts/ChartToolbar';
|
||||
import ToolbarPortal from '../utility/ToolbarPortal';
|
||||
|
||||
export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database, tabid }) {
|
||||
const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel());
|
||||
const saveFileModalState = useModalState();
|
||||
const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({
|
||||
tabid,
|
||||
});
|
||||
@@ -57,20 +56,17 @@ export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database
|
||||
database={database}
|
||||
/>
|
||||
<SaveTabModal
|
||||
modalState={saveFileModalState}
|
||||
tabVisible={tabVisible}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
data={modelState.value}
|
||||
format="json"
|
||||
folder="charts"
|
||||
tabid={tabid}
|
||||
fileExtension="chart"
|
||||
/>
|
||||
{toolbarPortalRef &&
|
||||
toolbarPortalRef.current &&
|
||||
tabVisible &&
|
||||
ReactDOM.createPortal(
|
||||
<ChartToolbar save={saveFileModalState.open} modelState={modelState} dispatchModel={dispatchModel} />,
|
||||
toolbarPortalRef.current
|
||||
)}
|
||||
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
|
||||
<ChartToolbar modelState={modelState} dispatchModel={dispatchModel} />
|
||||
</ToolbarPortal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import useEditorData from '../utility/useEditorData';
|
||||
export default function FreeDataTab({ archiveFolder, archiveFile, tabVisible, toolbarPortalRef, tabid, initialArgs }) {
|
||||
const [config, setConfig] = useGridConfig(tabid);
|
||||
const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel());
|
||||
const saveFileModalState = useModalState();
|
||||
const saveArchiveModalState = useModalState();
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({
|
||||
tabid,
|
||||
@@ -59,9 +59,9 @@ export default function FreeDataTab({ archiveFolder, archiveFile, tabVisible, to
|
||||
dispatchModel={dispatchModel}
|
||||
tabVisible={tabVisible}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
onSave={() => saveFileModalState.open()}
|
||||
onSave={() => saveArchiveModalState.open()}
|
||||
/>
|
||||
<SaveArchiveModal modalState={saveFileModalState} folder={archiveFolder} file={archiveFile} onSave={handleSave} />
|
||||
<SaveArchiveModal modalState={saveArchiveModalState} folder={archiveFolder} file={archiveFile} onSave={handleSave} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ import LoadingInfo from '../widgets/LoadingInfo';
|
||||
import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
import { setSelectedTabFunc } from '../utility/common';
|
||||
import ToolbarPortal from '../utility/ToolbarPortal';
|
||||
|
||||
export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef, ...other }) {
|
||||
const { editorData, setEditorData, isLoading, saveToStorage } = useEditorData({ tabid });
|
||||
const saveFileModalState = useModalState();
|
||||
const openedTabs = useOpenedTabs();
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const openNewTab = useOpenNewTab();
|
||||
@@ -61,20 +61,17 @@ export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef,
|
||||
onKeyDown={handleKeyDown}
|
||||
mode="markdown"
|
||||
/>
|
||||
{toolbarPortalRef &&
|
||||
toolbarPortalRef.current &&
|
||||
tabVisible &&
|
||||
ReactDOM.createPortal(
|
||||
<MarkdownToolbar save={saveFileModalState.open} showPreview={showPreview} />,
|
||||
toolbarPortalRef.current
|
||||
)}
|
||||
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
|
||||
<MarkdownToolbar showPreview={showPreview} />
|
||||
</ToolbarPortal>
|
||||
<SaveTabModal
|
||||
modalState={saveFileModalState}
|
||||
tabVisible={tabVisible}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
data={editorData}
|
||||
format="text"
|
||||
folder="markdown"
|
||||
tabid={tabid}
|
||||
fileExtension="md"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,6 @@ import keycodes from '../utility/keycodes';
|
||||
import { changeTab } from '../utility/common';
|
||||
import useSocket from '../utility/SocketProvider';
|
||||
import SaveTabModal from '../modals/SaveTabModal';
|
||||
import useModalState from '../modals/useModalState';
|
||||
import sqlFormatter from 'sql-formatter';
|
||||
import useEditorData from '../utility/useEditorData';
|
||||
import LoadingInfo from '../widgets/LoadingInfo';
|
||||
@@ -25,15 +24,25 @@ import QueryDesignColumns from '../designer/QueryDesignColumns';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import { generateDesignedQuery } from '../designer/designerTools';
|
||||
import useUndoReducer from '../utility/useUndoReducer';
|
||||
import { StatusBarItem } from '../widgets/StatusBar';
|
||||
import useTimerLabel from '../utility/useTimerLabel';
|
||||
import ToolbarPortal from '../utility/ToolbarPortal';
|
||||
|
||||
export default function QueryDesignTab({ tabid, conid, database, tabVisible, toolbarPortalRef, ...other }) {
|
||||
export default function QueryDesignTab({
|
||||
tabid,
|
||||
conid,
|
||||
database,
|
||||
tabVisible,
|
||||
toolbarPortalRef,
|
||||
statusbarPortalRef,
|
||||
...other
|
||||
}) {
|
||||
const [sessionId, setSessionId] = React.useState(null);
|
||||
const [visibleResultTabs, setVisibleResultTabs] = React.useState(false);
|
||||
const [executeNumber, setExecuteNumber] = React.useState(0);
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const socket = useSocket();
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const saveFileModalState = useModalState();
|
||||
const extensions = useExtensions();
|
||||
const connection = useConnectionInfo({ conid });
|
||||
const engine = findEngineDriver(connection, extensions);
|
||||
@@ -49,6 +58,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
|
||||
},
|
||||
{ mergeNearActions: true }
|
||||
);
|
||||
const timerLabel = useTimerLabel();
|
||||
|
||||
React.useEffect(() => {
|
||||
// @ts-ignore
|
||||
@@ -61,6 +71,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
|
||||
|
||||
const handleSessionDone = React.useCallback(() => {
|
||||
setBusy(false);
|
||||
timerLabel.stop();
|
||||
}, []);
|
||||
|
||||
const generatePreview = (value, engine) => {
|
||||
@@ -114,6 +125,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
|
||||
setSessionId(sesid);
|
||||
}
|
||||
setBusy(true);
|
||||
timerLabel.start();
|
||||
await axios.post('sessions/execute-query', {
|
||||
sesid,
|
||||
sql: sqlPreview,
|
||||
@@ -126,6 +138,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
|
||||
});
|
||||
setSessionId(null);
|
||||
setBusy(false);
|
||||
timerLabel.stop();
|
||||
};
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
@@ -182,31 +195,32 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
|
||||
)}
|
||||
</ResultTabs>
|
||||
</VerticalSplitter>
|
||||
{toolbarPortalRef &&
|
||||
toolbarPortalRef.current &&
|
||||
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
|
||||
<QueryDesignToolbar
|
||||
modelState={modelState}
|
||||
dispatchModel={dispatchModel}
|
||||
isDatabaseDefined={conid && database}
|
||||
execute={handleExecute}
|
||||
busy={busy}
|
||||
// cancel={handleCancel}
|
||||
// format={handleFormatCode}
|
||||
isConnected={!!sessionId}
|
||||
kill={handleKill}
|
||||
/>
|
||||
</ToolbarPortal>
|
||||
{statusbarPortalRef &&
|
||||
statusbarPortalRef.current &&
|
||||
tabVisible &&
|
||||
ReactDOM.createPortal(
|
||||
<QueryDesignToolbar
|
||||
modelState={modelState}
|
||||
dispatchModel={dispatchModel}
|
||||
isDatabaseDefined={conid && database}
|
||||
execute={handleExecute}
|
||||
busy={busy}
|
||||
// cancel={handleCancel}
|
||||
// format={handleFormatCode}
|
||||
save={saveFileModalState.open}
|
||||
isConnected={!!sessionId}
|
||||
kill={handleKill}
|
||||
/>,
|
||||
toolbarPortalRef.current
|
||||
)}
|
||||
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
|
||||
<SaveTabModal
|
||||
modalState={saveFileModalState}
|
||||
// modalState={saveFileModalState}
|
||||
tabVisible={tabVisible}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
data={modelState.value}
|
||||
format="json"
|
||||
folder="query"
|
||||
tabid={tabid}
|
||||
fileExtension="qdesign"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -21,16 +21,47 @@ import useEditorData from '../utility/useEditorData';
|
||||
import applySqlTemplate from '../utility/applySqlTemplate';
|
||||
import LoadingInfo from '../widgets/LoadingInfo';
|
||||
import useExtensions from '../utility/useExtensions';
|
||||
import useTimerLabel from '../utility/useTimerLabel';
|
||||
import { StatusBarItem } from '../widgets/StatusBar';
|
||||
import ToolbarPortal from '../utility/ToolbarPortal';
|
||||
import { useShowMenu } from '../modals/showMenu';
|
||||
|
||||
export default function QueryTab({ tabid, conid, database, initialArgs, tabVisible, toolbarPortalRef, ...other }) {
|
||||
function createSqlPreview(sql) {
|
||||
if (!sql) return undefined;
|
||||
let data = sql.substring(0, 500);
|
||||
data = data.replace(/\[[^\]]+\]\./g, '');
|
||||
data = data.replace(/\[a-zA-Z0-9_]+\./g, '');
|
||||
data = data.replace(/\/\*.*\*\//g, '');
|
||||
data = data.replace(/[\[\]]/g, '');
|
||||
data = data.replace(/--[^\n]*\n/g, '');
|
||||
|
||||
for (let step = 1; step <= 5; step++) {
|
||||
data = data.replace(/\([^\(^\)]+\)/g, '');
|
||||
}
|
||||
data = data.replace(/\s+/g, ' ');
|
||||
data = data.trim();
|
||||
data = data.replace(/^(.{50}[^\s]*).*/, '$1');
|
||||
return data;
|
||||
}
|
||||
|
||||
export default function QueryTab({
|
||||
tabid,
|
||||
conid,
|
||||
database,
|
||||
initialArgs,
|
||||
tabVisible,
|
||||
toolbarPortalRef,
|
||||
statusbarPortalRef,
|
||||
...other
|
||||
}) {
|
||||
const [sessionId, setSessionId] = React.useState(null);
|
||||
const [visibleResultTabs, setVisibleResultTabs] = React.useState(false);
|
||||
const [executeNumber, setExecuteNumber] = React.useState(0);
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const socket = useSocket();
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const saveFileModalState = useModalState();
|
||||
const extensions = useExtensions();
|
||||
const timerLabel = useTimerLabel();
|
||||
const { editorData, setEditorData, isLoading } = useEditorData({
|
||||
tabid,
|
||||
loadFromArgs:
|
||||
@@ -43,6 +74,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
|
||||
|
||||
const handleSessionDone = React.useCallback(() => {
|
||||
setBusy(false);
|
||||
timerLabel.stop();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -61,6 +93,23 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
|
||||
useUpdateDatabaseForTab(tabVisible, conid, database);
|
||||
const connection = useConnectionInfo({ conid });
|
||||
|
||||
const updateContentPreviewDebounced = React.useRef(
|
||||
_.debounce(
|
||||
// @ts-ignore
|
||||
sql =>
|
||||
changeTab(tabid, setOpenedTabs, tab => ({
|
||||
...tab,
|
||||
contentPreview: createSqlPreview(sql),
|
||||
})),
|
||||
500
|
||||
)
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// @ts-ignore
|
||||
updateContentPreviewDebounced.current(editorData);
|
||||
}, [editorData]);
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (busy) return;
|
||||
setExecuteNumber(num => num + 1);
|
||||
@@ -77,11 +126,14 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
|
||||
setSessionId(sesid);
|
||||
}
|
||||
setBusy(true);
|
||||
timerLabel.start();
|
||||
await axios.post('sessions/execute-query', {
|
||||
sesid,
|
||||
sql: selectedText || editorData,
|
||||
});
|
||||
};
|
||||
// const handleExecuteRef = React.useRef(handleExecute);
|
||||
// handleExecuteRef.current = handleExecute;
|
||||
|
||||
// const handleCancel = () => {
|
||||
// axios.post('sessions/cancel', {
|
||||
@@ -95,10 +147,11 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
|
||||
});
|
||||
setSessionId(null);
|
||||
setBusy(false);
|
||||
timerLabel.stop();
|
||||
};
|
||||
|
||||
const handleKeyDown = (data, hash, keyString, keyCode, event) => {
|
||||
if (keyCode == keycodes.f5) {
|
||||
if (keyCode == keycodes.f5 || (keyCode == keycodes.enter && event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
handleExecute();
|
||||
}
|
||||
@@ -136,6 +189,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
|
||||
editorRef={editorRef}
|
||||
conid={conid}
|
||||
database={database}
|
||||
onExecute={handleExecute}
|
||||
/>
|
||||
{visibleResultTabs && (
|
||||
<ResultTabs sessionId={sessionId} executeNumber={executeNumber}>
|
||||
@@ -151,7 +205,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
|
||||
</ResultTabs>
|
||||
)}
|
||||
</VerticalSplitter>
|
||||
{toolbarPortalRef &&
|
||||
{/* {toolbarPortalRef &&
|
||||
toolbarPortalRef.current &&
|
||||
tabVisible &&
|
||||
ReactDOM.createPortal(
|
||||
@@ -166,14 +220,31 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
|
||||
kill={handleKill}
|
||||
/>,
|
||||
toolbarPortalRef.current
|
||||
)}
|
||||
)} */}
|
||||
{statusbarPortalRef &&
|
||||
statusbarPortalRef.current &&
|
||||
tabVisible &&
|
||||
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
|
||||
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
|
||||
<QueryToolbar
|
||||
isDatabaseDefined={conid && database}
|
||||
execute={handleExecute}
|
||||
busy={busy}
|
||||
// cancel={handleCancel}
|
||||
format={handleFormatCode}
|
||||
// save={saveFileModalState.open}
|
||||
isConnected={!!sessionId}
|
||||
kill={handleKill}
|
||||
/>
|
||||
</ToolbarPortal>
|
||||
<SaveTabModal
|
||||
modalState={saveFileModalState}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
tabVisible={tabVisible}
|
||||
data={editorData}
|
||||
format="text"
|
||||
folder="sql"
|
||||
tabid={tabid}
|
||||
fileExtension="sql"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -14,18 +14,20 @@ import useShowModal from '../modals/showModal';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import useEditorData from '../utility/useEditorData';
|
||||
import SaveTabModal from '../modals/SaveTabModal';
|
||||
import useModalState from '../modals/useModalState';
|
||||
import LoadingInfo from '../widgets/LoadingInfo';
|
||||
import useTimerLabel from '../utility/useTimerLabel';
|
||||
import { StatusBarItem } from '../widgets/StatusBar';
|
||||
import ToolbarPortal from '../utility/ToolbarPortal';
|
||||
|
||||
const configRegex = /\s*\/\/\s*@ImportExportConfigurator\s*\n\s*\/\/\s*(\{[^\n]+\})\n/;
|
||||
const requireRegex = /\s*(\/\/\s*@require\s+[^\n]+)\n/g;
|
||||
const initRegex = /([^\n]+\/\/\s*@init)/g;
|
||||
|
||||
export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other }) {
|
||||
export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, statusbarPortalRef, ...other }) {
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const showModal = useShowModal();
|
||||
const { editorData, setEditorData, isLoading } = useEditorData({ tabid });
|
||||
const saveFileModalState = useModalState();
|
||||
const timerLabel = useTimerLabel();
|
||||
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
|
||||
@@ -42,6 +44,7 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other
|
||||
|
||||
const handleRunnerDone = React.useCallback(() => {
|
||||
setBusy(false);
|
||||
timerLabel.stop();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -69,12 +72,14 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other
|
||||
runid = resp.data.runid;
|
||||
setRunnerId(runid);
|
||||
setBusy(true);
|
||||
timerLabel.start();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
axios.post('runners/cancel', {
|
||||
runid: runnerId,
|
||||
});
|
||||
timerLabel.stop();
|
||||
};
|
||||
|
||||
const handleKeyDown = (data, hash, keyString, keyCode, event) => {
|
||||
@@ -114,27 +119,27 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other
|
||||
/>
|
||||
<RunnerOutputPane runnerId={runnerId} executeNumber={executeNumber} />
|
||||
</VerticalSplitter>
|
||||
{toolbarPortalRef &&
|
||||
toolbarPortalRef.current &&
|
||||
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
|
||||
<ShellToolbar
|
||||
execute={handleExecute}
|
||||
busy={busy}
|
||||
cancel={handleCancel}
|
||||
edit={handleEdit}
|
||||
editAvailable={configRegex.test(editorData || '')}
|
||||
/>
|
||||
</ToolbarPortal>
|
||||
{statusbarPortalRef &&
|
||||
statusbarPortalRef.current &&
|
||||
tabVisible &&
|
||||
ReactDOM.createPortal(
|
||||
<ShellToolbar
|
||||
execute={handleExecute}
|
||||
busy={busy}
|
||||
cancel={handleCancel}
|
||||
edit={handleEdit}
|
||||
editAvailable={configRegex.test(editorData || '')}
|
||||
save={saveFileModalState.open}
|
||||
/>,
|
||||
toolbarPortalRef.current
|
||||
)}
|
||||
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
|
||||
<SaveTabModal
|
||||
modalState={saveFileModalState}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
tabVisible={tabVisible}
|
||||
data={editorData}
|
||||
format="text"
|
||||
folder="shell"
|
||||
tabid={tabid}
|
||||
fileExtension="js"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -50,6 +50,10 @@ export default function ThemeHelmet() {
|
||||
border: 1px solid ${theme.border};
|
||||
}
|
||||
|
||||
select[disabled] {
|
||||
background-color: ${theme.input_background2};
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: ${theme.input_background};
|
||||
color: ${theme.input_font1};
|
||||
|
||||
@@ -7,9 +7,11 @@ export default function ConnectionsPinger({ children }) {
|
||||
const openedConnections = useOpenedConnections();
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
|
||||
const doPing = () => {
|
||||
const doServerPing = () => {
|
||||
axios.post('server-connections/ping', { connections: openedConnections });
|
||||
};
|
||||
|
||||
const doDatabasePing = () => {
|
||||
const database = _.get(currentDatabase, 'name');
|
||||
const conid = _.get(currentDatabase, 'connection._id');
|
||||
if (conid && database) {
|
||||
@@ -18,9 +20,16 @@ export default function ConnectionsPinger({ children }) {
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
doPing();
|
||||
const handle = window.setInterval(doPing, 30 * 1000);
|
||||
doServerPing();
|
||||
const handle = window.setInterval(doServerPing, 30 * 1000);
|
||||
return () => window.clearInterval(handle);
|
||||
}, [openedConnections, currentDatabase]);
|
||||
}, [openedConnections]);
|
||||
|
||||
React.useEffect(() => {
|
||||
doDatabasePing();
|
||||
const handle = window.setInterval(doDatabasePing, 30 * 1000);
|
||||
return () => window.clearInterval(handle);
|
||||
}, [currentDatabase]);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import ToolbarButton, { ToolbarDropDownButton } from '../widgets/ToolbarButton';
|
||||
|
||||
export default function SaveFileToolbarButton({ tabid, save, saveAs }) {
|
||||
if (!saveAs) return null;
|
||||
|
||||
if (save) {
|
||||
return (
|
||||
<ToolbarDropDownButton icon="icon save" text="Save">
|
||||
<DropDownMenuItem onClick={save} keyText="Ctrl+S">
|
||||
Save
|
||||
</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={saveAs} keyText="Ctrl+Shift+S">
|
||||
Save As
|
||||
</DropDownMenuItem>
|
||||
</ToolbarDropDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton onClick={saveAs} icon="icon save">
|
||||
Save As
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
export default function ToolbarPortal({ toolbarPortalRef, tabVisible, children }) {
|
||||
return (
|
||||
(toolbarPortalRef &&
|
||||
toolbarPortalRef.current &&
|
||||
tabVisible &&
|
||||
children &&
|
||||
ReactDOM.createPortal(children, toolbarPortalRef.current)) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import { useDropzone } from 'react-dropzone';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import { findFileFormat } from './fileformats';
|
||||
import getElectron from './getElectron';
|
||||
import resolveApi from './resolveApi';
|
||||
import useExtensions from './useExtensions';
|
||||
import { useOpenElectronFileCore, canOpenByElectron } from './useOpenElectronFile';
|
||||
|
||||
const UploadsContext = React.createContext(null);
|
||||
|
||||
@@ -21,6 +23,8 @@ export function useUploadFiles() {
|
||||
const { uploadListener } = useUploadsProvider();
|
||||
const showModal = useShowModal();
|
||||
const extensions = useExtensions();
|
||||
const electron = getElectron();
|
||||
const openElectronFileCore = useOpenElectronFileCore();
|
||||
|
||||
const handleUploadFiles = React.useCallback(
|
||||
files => {
|
||||
@@ -31,6 +35,12 @@ export function useUploadFiles() {
|
||||
}
|
||||
|
||||
console.log('FILE', file);
|
||||
|
||||
if (electron && canOpenByElectron(file.path, extensions)) {
|
||||
openElectronFileCore(file.path);
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('data', file);
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// @ts-nocheck
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const FlexCol3 = styled.div`
|
||||
flex-basis: 25%;
|
||||
max-width: 25%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
export const FlexCol4 = styled.div`
|
||||
flex-basis: 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
export const FlexCol6 = styled.div`
|
||||
flex-basis: 50%;
|
||||
max-width: 50%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
export const FlexCol8 = styled.div`
|
||||
flex-basis: 66.6667%;
|
||||
max-width: 66.6667%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
export const FlexCol9 = styled.div`
|
||||
flex-basis: 75%;
|
||||
max-width: 75%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
@@ -14,10 +14,10 @@ export const FormLabel = styled.div`
|
||||
|
||||
export const FormValue = styled.div``;
|
||||
|
||||
export function FormFieldTemplateDefault({ label, children, type }) {
|
||||
export function FormFieldTemplateDefault({ label, children, labelProps, type }) {
|
||||
return (
|
||||
<FormRow>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<FormLabel {...labelProps}>{label}</FormLabel>
|
||||
<FormValue>{children}</FormValue>
|
||||
</FormRow>
|
||||
);
|
||||
@@ -36,19 +36,87 @@ export const FormValueTiny = styled.div`
|
||||
margin-top: 3px;
|
||||
`;
|
||||
|
||||
export function FormFieldTemplateTiny({ label, children, type }) {
|
||||
const FormLabelSpan = styled.span`
|
||||
${props =>
|
||||
// @ts-ignore
|
||||
props.disabled &&
|
||||
`
|
||||
color: ${props.theme.manager_font3};
|
||||
`}
|
||||
`;
|
||||
|
||||
export function FormFieldTemplateTiny({ label, children, labelProps, type }) {
|
||||
const theme = useTheme();
|
||||
if (type == 'checkbox') {
|
||||
return (
|
||||
<FormRowTiny>
|
||||
{children} {label}
|
||||
{children}{' '}
|
||||
<FormLabelSpan theme={theme} {...labelProps}>
|
||||
{label}
|
||||
</FormLabelSpan>
|
||||
</FormRowTiny>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormRowTiny>
|
||||
<FormLabelTiny theme={theme}>{label}</FormLabelTiny>
|
||||
<FormLabelTiny theme={theme}>
|
||||
<FormLabelSpan theme={theme} {...labelProps}>
|
||||
{label}
|
||||
</FormLabelSpan>
|
||||
</FormLabelTiny>
|
||||
<FormValueTiny>{children}</FormValueTiny>
|
||||
</FormRowTiny>
|
||||
);
|
||||
}
|
||||
|
||||
const FormRowLargeTemplate = styled.div`
|
||||
${props =>
|
||||
// @ts-ignore
|
||||
!props.noMargin &&
|
||||
`
|
||||
margin: 20px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const FormRowLarge = styled.div`
|
||||
margin: 20px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const FormLabelLarge = styled.div`
|
||||
margin-bottom: 3px;
|
||||
color: ${props => props.theme.manager_font3};
|
||||
`;
|
||||
|
||||
export const FormValueLarge = styled.div``;
|
||||
|
||||
export function FormFieldTemplateLarge({ label, labelProps, children, type, noMargin = false }) {
|
||||
const theme = useTheme();
|
||||
if (type == 'checkbox') {
|
||||
return (
|
||||
<FormRowLargeTemplate
|
||||
// @ts-ignore
|
||||
noMargin={noMargin}
|
||||
>
|
||||
{children}{' '}
|
||||
<FormLabelSpan {...labelProps} theme={theme}>
|
||||
{label}
|
||||
</FormLabelSpan>
|
||||
</FormRowLargeTemplate>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormRowLargeTemplate
|
||||
className="largeFormMarker"
|
||||
// @ts-ignore
|
||||
noMargin={noMargin}
|
||||
>
|
||||
<FormLabelLarge theme={theme}>
|
||||
<FormLabelSpan theme={theme} {...labelProps}>
|
||||
{label}
|
||||
</FormLabelSpan>
|
||||
</FormLabelLarge>
|
||||
<FormValueLarge>{children}</FormValueLarge>
|
||||
</FormRowLargeTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,13 @@ import axios from './axios';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import { useForm, useFormFieldTemplate } from './FormProvider';
|
||||
import { FontIcon } from '../icons';
|
||||
import getElectron from './getElectron';
|
||||
import InlineButton from '../widgets/InlineButton';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FlexContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export function FormFieldTemplate({ label, children, type }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
@@ -58,7 +65,7 @@ export function FormPasswordFieldRaw({ name, focused = false, ...other }) {
|
||||
const isCrypted = value && value.startsWith('crypt:');
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlexContainer>
|
||||
<TextField
|
||||
{...other}
|
||||
value={isCrypted ? '' : value}
|
||||
@@ -67,25 +74,28 @@ export function FormPasswordFieldRaw({ name, focused = false, ...other }) {
|
||||
placeholder={isCrypted ? '(Password is encrypted)' : undefined}
|
||||
type={isCrypted || showPassword ? 'text' : 'password'}
|
||||
/>
|
||||
|
||||
{!isCrypted && <FontIcon icon="icon eye" onClick={() => setShowPassword(x => !x)} />}
|
||||
</>
|
||||
{!isCrypted && (
|
||||
<InlineButton onClick={() => setShowPassword(x => !x)} disabled={other.disabled}>
|
||||
<FontIcon icon="icon eye" />
|
||||
</InlineButton>
|
||||
)}
|
||||
</FlexContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormTextField({ name, label, focused = false, ...other }) {
|
||||
export function FormTextField({ name, label, focused = false, templateProps = undefined, ...other }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type="text">
|
||||
<FieldTemplate label={label} type="text" {...templateProps}>
|
||||
<FormTextFieldRaw name={name} focused={focused} {...other} />
|
||||
</FieldTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormPasswordField({ name, label, focused = false, ...other }) {
|
||||
export function FormPasswordField({ name, label, focused = false, templateProps = undefined, ...other }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type="text">
|
||||
<FieldTemplate label={label} type="text" {...templateProps}>
|
||||
<FormPasswordFieldRaw name={name} focused={focused} {...other} />
|
||||
</FieldTemplate>
|
||||
);
|
||||
@@ -102,10 +112,18 @@ export function FormCheckboxFieldRaw({ name = undefined, defaultValue = undefine
|
||||
// return <Field {...other} as={CheckboxField} />;
|
||||
}
|
||||
|
||||
export function FormCheckboxField({ label, ...other }) {
|
||||
export function FormCheckboxField({ label, templateProps = undefined, ...other }) {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type="checkbox">
|
||||
<FieldTemplate
|
||||
label={label}
|
||||
type="checkbox"
|
||||
labelProps={
|
||||
other.disabled ? { disabled: true } : { onClick: () => setFieldValue(other.name, !values[other.name]) }
|
||||
}
|
||||
{...templateProps}
|
||||
>
|
||||
<FormCheckboxFieldRaw {...other} />
|
||||
</FieldTemplate>
|
||||
);
|
||||
@@ -123,10 +141,10 @@ export function FormSelectFieldRaw({ children, name, ...other }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function FormSelectField({ label, name, children = null, ...other }) {
|
||||
export function FormSelectField({ label, name, children = null, templateProps = undefined, ...other }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type="select">
|
||||
<FieldTemplate label={label} type="select" {...templateProps}>
|
||||
<FormSelectFieldRaw name={name} {...other}>
|
||||
{children}
|
||||
</FormSelectFieldRaw>
|
||||
@@ -321,3 +339,32 @@ export function FormArchiveFolderSelect({ name, additionalFolders = [], ...other
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormElectronFileSelectorRaw({ name, ...other }) {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const handleBrowse = () => {
|
||||
const electron = getElectron();
|
||||
if (!electron) return;
|
||||
const filePaths = electron.remote.dialog.showOpenDialogSync(electron.remote.getCurrentWindow(), {
|
||||
defaultPath: values[name],
|
||||
properties: ['showHiddenFiles'],
|
||||
});
|
||||
const filePath = filePaths && filePaths[0];
|
||||
if (filePath) setFieldValue(name, filePath);
|
||||
};
|
||||
return (
|
||||
<FlexContainer>
|
||||
<TextField value={values[name]} onClick={handleBrowse} {...other} readOnly />
|
||||
<InlineButton onClick={handleBrowse} disabled={other.disabled}>Browse</InlineButton>
|
||||
</FlexContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormElectronFileSelector({ label, name, templateProps = undefined, ...other }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type="select" {...templateProps}>
|
||||
<FormElectronFileSelectorRaw name={name} {...other} />
|
||||
</FieldTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,12 @@ const configLoader = () => ({
|
||||
reloadTrigger: 'config-changed',
|
||||
});
|
||||
|
||||
const platformInfoLoader = () => ({
|
||||
url: 'config/platform-info',
|
||||
params: {},
|
||||
reloadTrigger: 'platform-info-changed',
|
||||
});
|
||||
|
||||
const favoritesLoader = () => ({
|
||||
url: 'files/favorites',
|
||||
params: {},
|
||||
@@ -253,6 +259,13 @@ export function useConfig() {
|
||||
return useCore(configLoader, {}) || {};
|
||||
}
|
||||
|
||||
export function getPlatformInfo() {
|
||||
return getCore(platformInfoLoader, {}) || {};
|
||||
}
|
||||
export function usePlatformInfo() {
|
||||
return useCore(platformInfoLoader, {}) || {};
|
||||
}
|
||||
|
||||
export function getArchiveFiles(args) {
|
||||
return getCore(archiveFilesLoader, args);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
|
||||
setValue(init);
|
||||
valueRef.current = init;
|
||||
initialDataRef.current = init;
|
||||
// mark as not saved
|
||||
changeCounterRef.current += 1;
|
||||
} catch (err) {
|
||||
const message = (err && err.response && err.response.data && err.response.data.error) || 'Loading failed';
|
||||
setErrorMessage(message);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user