Compare commits

..

123 Commits

Author SHA1 Message Date
Jan Prochazka 4cb304a41c v3.9.6 2021-02-15 18:38:49 +01:00
Jan Prochazka abdbe4122f changelog 2021-02-15 18:37:50 +01:00
Jan Prochazka b2e37e88ea fix 2021-02-15 18:37:13 +01:00
Jan Prochazka 6a28ceaa51 v3.9.6-beta.7 2021-02-15 06:41:03 +01:00
Jan Prochazka c15f859eae #63 - clean solution - added ...process.env to fork(env) 2021-02-15 06:40:41 +01:00
Jan Prochazka f717dfa3b5 readme 2021-02-14 21:18:35 +01:00
Jan Prochazka ab7510c8e8 readme 2021-02-14 21:15:59 +01:00
Jan Prochazka 3601ac21b4 changelog 2021-02-14 21:09:08 +01:00
Jan Prochazka 65bd5a60ef v3.9.6-beta.6 2021-02-14 19:26:49 +01:00
Jan Prochazka 6a9bd8248c #63 - workaround export from SNAP 2021-02-14 19:26:00 +01:00
Jan Prochazka 98ff6db701 runners cwd simplified 2021-02-14 19:09:52 +01:00
Jan Prochazka 7b67576131 Revert "electron changed to version 9"
This reverts commit 1f057fb0a9.
2021-02-14 18:09:36 +01:00
Jan Prochazka d0d6d86bb9 v3.9.6-beta.5 2021-02-14 11:51:00 +01:00
Jan Prochazka 1f057fb0a9 electron changed to version 9 2021-02-14 11:50:29 +01:00
Jan Prochazka 75a429b74f build npm only on final release 2021-02-14 11:46:29 +01:00
Jan Prochazka d34524c3d0 v3.9.6-alpha.14 2021-02-14 11:37:57 +01:00
Jan Prochazka bdfa66d37d v3.9.6-beta.4 2021-02-14 11:37:31 +01:00
Jan Prochazka 1d5d87e26a try to upgrade electron 2021-02-14 11:37:06 +01:00
Jan Prochazka 9f3aadc17d fixed exports in dbgate NPM package 2021-02-14 11:33:53 +01:00
Jan Prochazka 58f213d042 v3.9.6-beta.3 2021-02-14 11:10:27 +01:00
Jan Prochazka 89dbf38962 try to fix export in electron 2021-02-14 11:09:49 +01:00
Jan Prochazka 670e3d127e sql editor context menu 2021-02-14 10:50:55 +01:00
Jan Prochazka 72181e70a1 #63 2021-02-14 09:41:32 +01:00
Jan Prochazka c3ac836fa9 upgrade plugin dependencies 2021-02-14 09:37:59 +01:00
Jan Prochazka b0deba4bae style 2021-02-14 09:26:24 +01:00
Jan Prochazka d08fc85459 SSL support 2021-02-14 09:11:40 +01:00
Jan Prochazka 417ec9fcd2 connection modal style 2021-02-13 19:56:04 +01:00
Jan Prochazka f9d4a9a3a0 connection modal style 2021-02-13 19:46:15 +01:00
Jan Prochazka eab870c237 connection form style 2021-02-13 19:05:37 +01:00
Jan Prochazka 521199ee1a handle ssh tunnel error 2021-02-13 12:46:37 +01:00
Jan Prochazka 0d1a6e96f3 code format 2021-02-13 12:14:23 +01:00
Jan Prochazka 1076fb8391 ssh tunnel - alternative modes 2021-02-13 12:13:10 +01:00
Jan Prochazka 114dc0b543 ssh tunnel - reuse SSH connection + local port for multiple DB connections 2021-02-13 07:47:55 +01:00
Jan Prochazka 728ca72cc1 ssh tunnel - wking POC 2021-02-11 11:34:54 +01:00
Jan Prochazka e243ecd96a fixed repo links 2021-02-11 10:11:34 +01:00
Jan Prochazka 2defdc3f28 issue templates 2021-02-08 19:58:22 +01:00
Jan Prochazka 7aeef55a58 Update issue templates 2021-02-08 19:55:51 +01:00
Jan Prochazka 5e8967da52 issue templates 2021-02-08 19:51:27 +01:00
Jan Prochazka 679145a394 v3.9.6-alpha.13 2021-02-08 19:22:56 +01:00
Jan Prochazka 9b012c187a v3.9.6-beta.2 2021-02-08 19:22:36 +01:00
Jan Prochazka 9ce1fdd59e version in reposirory should be last stable version 2021-02-08 19:22:13 +01:00
Jan Prochazka c0d0a00615 try to fix electron export 2021-02-08 19:21:35 +01:00
Jan Prochazka c67c08bd69 v3.9.6-beta.1 2021-02-08 18:48:59 +01:00
Jan Prochazka 6e846797b9 v3.9.6-alpha.12 2021-02-08 18:37:38 +01:00
Jan Prochazka a3ad98d2a9 missing dependency 2021-02-08 18:37:27 +01:00
Jan Prochazka c7dbf333c7 v3.9.6-alpha.11 2021-02-08 18:28:46 +01:00
Jan Prochazka 19392e9406 fixes + optimalized web package 2021-02-08 18:27:26 +01:00
Jan Prochazka 818de9b111 v3.9.6-alpha.10 2021-02-08 18:08:24 +01:00
Jan Prochazka 0292a37b16 readme 2021-02-08 18:08:07 +01:00
Jan Prochazka 8b9031b0c2 npm token refactor 2021-02-08 17:59:37 +01:00
Jan Prochazka d88591032e web can be run from dbgate package 2021-02-08 17:55:09 +01:00
Jan Prochazka 2c6a59638b fix 2021-02-08 17:26:27 +01:00
Jan Prochazka 8312415430 v3.9.6-alpha.9 2021-02-08 17:15:33 +01:00
Jan Prochazka fdb14d687b setCurrentVersion script 2021-02-08 17:15:08 +01:00
Jan Prochazka a20b351938 v3.9.6-alpha.9 2021-02-07 17:53:19 +01:00
Jan Prochazka f8016d26ec fix 2021-02-07 17:52:51 +01:00
Jan Prochazka ad186f5efb v3.9.6-alpha.8 2021-02-07 17:49:02 +01:00
Jan Prochazka 8e26918975 fixes 2021-02-07 17:48:46 +01:00
Jan Prochazka e0303aa77e v3.9.6-alpha.7 2021-02-07 17:37:24 +01:00
Jan Prochazka 6a02c4ebaa fix 2021-02-07 17:37:06 +01:00
Jan Prochazka 2c73ab6bc1 v3.9.6-alpha.6 2021-02-07 17:25:05 +01:00
Jan Prochazka b60714f30c npm build dbgate 2021-02-07 17:24:45 +01:00
Jan Prochazka f3163617e0 v3.9.6-alpha.5 2021-02-07 10:52:33 +01:00
Jan Prochazka 834be32676 npmrc file 2021-02-07 10:52:20 +01:00
Jan Prochazka 0f6637188b v3.9.6-alpha.4 2021-02-07 10:43:47 +01:00
Jan Prochazka ef0921ecf5 try to fix 2021-02-07 10:43:36 +01:00
Jan Prochazka 1ffa613e09 v3.9.6-alpha.3 2021-02-07 10:33:13 +01:00
Jan Prochazka c0c8cd88e3 npm token 2021-02-07 10:32:58 +01:00
Jan Prochazka 2666717c3a v3.9.6-alpha.2 2021-02-07 10:19:15 +01:00
Jan Prochazka 777abbc097 v3.9.6-aplha.2 2021-02-07 10:18:21 +01:00
Jan Prochazka e4db985ef9 fix 2021-02-07 10:17:49 +01:00
Jan Prochazka 6afaa6f856 v3.9.6-alpha.1 2021-02-07 10:13:24 +01:00
Jan Prochazka 1325851bcf npm build 2021-02-07 10:13:07 +01:00
Jan Prochazka fb7da60127 fix 2021-02-07 10:01:15 +01:00
Jan Prochazka ecde9cb6bd fix 2021-02-07 09:58:57 +01:00
Jan Prochazka 6b06ed5baf set current version 2021-02-07 09:56:50 +01:00
Jan Prochazka 2aa965cf3b license, API - prepare to run from dbgate npm package 2021-02-07 09:44:42 +01:00
Jan Prochazka fc11fe1e8d readme 2021-02-04 16:23:11 +01:00
Jan Prochazka e33e14bd5f v3.9.5 2021-02-01 19:49:00 +01:00
Jan Prochazka 0c7fc0b7b6 v3.9.5-beta.2 2021-02-01 19:35:06 +01:00
Jan Prochazka 5904d45c44 Revert "upgraded electron - fixed problem with deleted localstorage"
This reverts commit 84e475192e.
2021-02-01 18:28:38 +01:00
Jan Prochazka 5d997fc1c9 revert 2021-02-01 18:28:16 +01:00
Jan Prochazka 4693564ffa v3.9.5-beta.1 2021-02-01 19:08:12 +01:00
Jan Prochazka 3377929b30 v0.0.0 2021-02-01 19:08:01 +01:00
Jan Prochazka a35f6f2629 reverted - private package 2021-02-01 19:07:46 +01:00
Jan Prochazka cb264ac6cc v3.9.4 2021-02-01 18:41:31 +01:00
Jan Prochazka 9236e1a6c2 Merge branch 'develop' 2021-02-01 18:40:20 +01:00
Jan Prochazka df359aea58 reverted try to dbgate global package 2021-02-01 18:39:55 +01:00
Jan Prochazka fdf60b5267 dbgate package 2021-02-01 18:09:57 +01:00
Jan Prochazka bd3c18d883 v3.9.4-beta.5 2021-01-31 09:24:27 +01:00
Jan Prochazka 18bf6e5979 open data files using open dialog in electron + drag & drop in electron without uploading 2021-01-31 09:21:54 +01:00
Jan Prochazka edaf9676e4 readme 2021-01-31 08:00:37 +01:00
Jan Prochazka bd524d345a readme 2021-01-31 07:51:20 +01:00
Jan Prochazka 0a39a6829c screenshot 2021-01-31 07:46:50 +01:00
Jan Prochazka 5c7a011efb report problem menu 2021-01-31 07:08:15 +01:00
Jan Prochazka 4e350e99c4 v3.9.4-beta.4 2021-01-30 20:07:28 +01:00
Jan Prochazka a714f7ae54 small refactor 2021-01-30 20:05:50 +01:00
Jan Prochazka a17b76c570 save from electron menu 2021-01-30 20:01:54 +01:00
Jan Prochazka 54d476a972 open sql file with drag & drop 2021-01-30 19:06:30 +01:00
Jan Prochazka 255c3e5ef4 improved save file experience 2021-01-30 18:23:05 +01:00
Jan Prochazka 059eabf2fa rename 2021-01-30 12:49:02 +01:00
Jan Prochazka 79fdde73ae v3.9.4-beta.3 2021-01-30 11:01:03 +01:00
Jan Prochazka 84e475192e upgraded electron - fixed problem with deleted localstorage 2021-01-30 10:56:22 +01:00
Jan Prochazka 3907b1ae8b v3.9.4-beta.2 2021-01-30 10:43:07 +01:00
Jan Prochazka dcfefc78a2 fixed save generated content in useEditorData 2021-01-30 10:37:28 +01:00
Jan Prochazka d3039a9248 useStorage improved - setter never changes (behaves more like useState) 2021-01-30 10:29:56 +01:00
Jan Prochazka 31dd80b79a fixed qorking with tabs 2021-01-30 09:41:50 +01:00
Jan Prochazka 8d6d1d979e v3.9.4-beta.1 2021-01-28 18:54:15 +01:00
Jan Prochazka fe1c5f5801 electron: save file to custom location 2021-01-28 18:52:59 +01:00
Jan Prochazka df976a84d2 electron menu sinplified 2021-01-28 16:56:47 +01:00
Jan Prochazka 420e94600e closed tab - show more info 2021-01-28 16:41:30 +01:00
Jan Prochazka 9940bd5177 upgrade mysql plugin dependency 2021-01-28 15:29:45 +01:00
Jan Prochazka 45d99a4126 timer labels in query design tab and shell tab 2021-01-28 13:00:24 +01:00
Jan Prochazka c2b7c775c0 code cleanup 2021-01-28 12:56:06 +01:00
Jan Prochazka 51ba9d3b5a statusbar - show query execution duration 2021-01-28 12:49:00 +01:00
Jan Prochazka 8396e726ec numbering tabs 2021-01-28 10:10:41 +01:00
Jan Prochazka cb67b57faf numbering tabs fix 2021-01-28 10:07:02 +01:00
Jan Prochazka 99381536d7 numbering tabs 2021-01-28 10:05:27 +01:00
Jan Prochazka a9cb9f1874 style fix 2021-01-28 09:48:33 +01:00
Jan Prochazka 420a58380a upgraded required plugin-postgres version 2021-01-28 09:44:48 +01:00
Jan Prochazka 75ca3cbb11 packages-tools v1.0.8 2021-01-28 09:12:38 +01:00
Jan Prochazka a5c1966a94 makeUniqueColumnNames function 2021-01-28 09:12:10 +01:00
Jan Prochazka ca4ff95316 optimalized connection ping 2021-01-28 08:16:31 +01:00
112 changed files with 2203 additions and 532 deletions
+33
View File
@@ -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
+20
View File
@@ -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.
+18
View File
@@ -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
+91
View File
@@ -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
+13
View File
@@ -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
+29 -45
View File
@@ -1,32 +1,41 @@
[![NPM version](https://img.shields.io/npm/v/dbgate.svg)](https://www.npmjs.com/package/dbgate)
[![dbgate](https://snapcraft.io/dbgate/badge.svg)](https://snapcraft.io/dbgate)
[![dbgate](https://snapcraft.io/dbgate/trending.svg?name=0)](https://snapcraft.io/dbgate)
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz/30eur)
[![NPM version](https://img.shields.io/npm/v/dbgate-api.svg)](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)
![Screenshot](https://raw.githubusercontent.com/dbgate/dbgate/master/screenshot.png)
## 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
![Screenshot](https://raw.githubusercontent.com/dbshell/dbgate/master/screenshot.png)
## 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 [![NPM version](https://img.shields.io/npm/v/dbgate-api.svg)](https://www.npmjs.com/package/dbgate-api)
* [datalib](https://github.com/dbshell/dbgate/tree/master/packages/datalib) - TypeScript library for utility classes
* [app](https://github.com/dbshell/dbgate/tree/master/app) - application (JavaScript)
structure, creating specific queries (JavaScript) [![NPM version](https://img.shields.io/npm/v/dbgate-engines.svg)](https://www.npmjs.com/package/dbgate-engines)
* [filterparser](https://github.com/dbshell/dbgate/tree/master/packages/filterparser) - TypeScript library for parsing data filter expressions using parsimmon
* [sqltree](https://github.com/dbshell/dbgate/tree/master/packages/sqltree) - JSON representation of SQL query, functions converting to SQL (TypeScript) [![NPM version](https://img.shields.io/npm/v/dbgate-sqltree.svg)](https://www.npmjs.com/package/dbgate-sqltree)
* [types](https://github.com/dbshell/dbgate/tree/master/packages/types) - common TypeScript definitions [![NPM version](https://img.shields.io/npm/v/dbgate-types.svg)](https://www.npmjs.com/package/dbgate-types)
* [web](https://github.com/dbshell/dbgate/tree/master/packages/web) - frontend in React (JavaScript)
* [tools](https://github.com/dbshell/dbgate/tree/master/packages/tools) - various tools [![NPM version](https://img.shields.io/npm/v/dbgate-tools.svg)](https://www.npmjs.com/package/dbgate-tools)
* [api](https://github.com/dbgate/dbgate/tree/master/packages/api) - backend, Javascript, ExpressJS [![NPM version](https://img.shields.io/npm/v/dbgate-api.svg)](https://www.npmjs.com/package/dbgate-api)
* [datalib](https://github.com/dbgate/dbgate/tree/master/packages/datalib) - TypeScript library for utility classes [![NPM version](https://img.shields.io/npm/v/dbgate-datalib.svg)](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 [![NPM version](https://img.shields.io/npm/v/dbgate-filterparser.svg)](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) [![NPM version](https://img.shields.io/npm/v/dbgate-sqltree.svg)](https://www.npmjs.com/package/dbgate-sqltree)
* [types](https://github.com/dbgate/dbgate/tree/master/packages/types) - common TypeScript definitions [![NPM version](https://img.shields.io/npm/v/dbgate-types.svg)](https://www.npmjs.com/package/dbgate-types)
* [web](https://github.com/dbgate/dbgate/tree/master/packages/web) - frontend in React (JavaScript) [![NPM version](https://img.shields.io/npm/v/dbgate-web.svg)](https://www.npmjs.com/package/dbgate-web)
* [tools](https://github.com/dbgate/dbgate/tree/master/packages/tools) - various tools [![NPM version](https://img.shields.io/npm/v/dbgate-tools.svg)](https://www.npmjs.com/package/dbgate-tools)
+13 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,5 +1,6 @@
{
"private": true,
"version": "3.9.6",
"name": "dbgate-all",
"workspaces": [
"packages/*"
+1 -1
View File
@@ -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
+10 -6
View File
@@ -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",
+7
View File
@@ -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);
});
+5
View File
@@ -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 [];
+3 -3
View File
@@ -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',
};
+9 -3
View File
@@ -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' });
})
+5 -2
View File
@@ -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 });
+4 -3
View File
@@ -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'
};
+4 -7
View File
@@ -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'),
};
+13 -3
View File
@@ -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);
}
+4 -2
View File
@@ -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) {
+4 -1
View File
@@ -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) {
+2 -1
View File
@@ -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)) {
+2
View File
@@ -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;
+2 -1
View File
@@ -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);
}
+2 -1
View File
@@ -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 };
+2 -1
View File
@@ -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);
}
+6 -2
View File
@@ -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;
+24 -10
View File
@@ -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;
}
+27
View File
@@ -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;
+18
View File
@@ -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,
};
+82
View File
@@ -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,
};
+4 -4
View File
@@ -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"
}
+28
View File
@@ -0,0 +1,28 @@
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz/30eur)
[![NPM version](https://img.shields.io/npm/v/dbgate.svg)](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
![Screenshot](https://raw.githubusercontent.com/dbgate/dbgate/master/screenshot.png)
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env node
const dbgateApi = require('dbgate-api');
global.dbgateApiModulePath = require.resolve('dbgate-api');
dbgateApi.getMainModule().start('startNodeWeb');
+25
View File
@@ -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"
}
}
+4 -4
View File
@@ -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"
}
}
}
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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"
}
}
}
+5 -5
View File
@@ -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"
}
}
}
+12
View File
@@ -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);
}
}
+4 -4
View File
@@ -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
View File
@@ -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"
}
}
}
+5 -7
View File
@@ -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>
+3 -2
View File
@@ -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 />
+10 -3
View File
@@ -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}
/>
);
+21 -1
View File
@@ -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)}
+30 -26
View File
@@ -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}
</>
);
}
+22 -8
View File
@@ -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;
+1 -1
View File
@@ -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 -9
View File
@@ -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>
+3 -3
View File
@@ -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: {},
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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,
+24
View File
@@ -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;
}
+12
View File
@@ -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 -9
View File
@@ -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>
+1 -1
View File
@@ -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">
+252 -35
View File
@@ -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 &amp; 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>
+7 -2
View File
@@ -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>
+15 -2
View File
@@ -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>
);
}
+58 -2
View File
@@ -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>
+82 -18
View File
@@ -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>
)}
</>
);
}
+3 -3
View File
@@ -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 -8
View File
@@ -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>
)}
</>
);
}
+2 -2
View File
@@ -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',
+54 -17
View File
@@ -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>
)}
</>
);
}
+6 -10
View File
@@ -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>
</>
);
}
+3 -3
View File
@@ -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} />
</>
);
}
+6 -9
View File
@@ -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"
/>
</>
);
+35 -21
View File
@@ -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"
/>
</>
);
+77 -6
View File
@@ -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"
/>
</>
);
+22 -17
View File
@@ -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"
/>
</>
);
+4
View File
@@ -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};
+13 -4
View File
@@ -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>
);
}
+13
View File
@@ -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);
+73
View 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;
`}
`;
+73 -5
View File
@@ -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>
);
}
+59 -12
View File
@@ -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