Compare commits

...

143 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
Jan Prochazka
a3294950a4 v3.9.3 2021-01-26 20:40:15 +01:00
Jan Prochazka
29355a6d3e v3.9.3-beta.1 2021-01-26 20:31:10 +01:00
Jan Prochazka
add0ba09c3 fix 2021-01-26 20:30:38 +01:00
Jan Prochazka
005ae87309 v3.9.2 2021-01-25 17:46:53 +01:00
Jan Prochazka
5f372a1d0f v3.9.2-beta.1 2021-01-25 17:33:42 +01:00
Jan Prochazka
ecce75960a fix in open ref table 2021-01-25 17:33:16 +01:00
Jan Prochazka
72cc510c64 improved error boundary 2021-01-25 17:20:17 +01:00
Jan Prochazka
7e39b8c2a0 v3.9.1 2021-01-25 06:52:06 +01:00
Jan Prochazka
ed4ef4d999 v3.9.1-beta.2 2021-01-24 17:26:28 +01:00
Jan Prochazka
0e6b8b4f73 imrpoved closed tabs order algorithm 2021-01-24 10:10:07 +01:00
Jan Prochazka
69fd9bbc67 fixed closing tabs 2021-01-24 09:58:58 +01:00
Jan Prochazka
e561bf38f6 tabs fix 2021-01-24 09:49:58 +01:00
Jan Prochazka
b3d436ddf9 filtering datetime values 2021-01-24 09:25:38 +01:00
Jan Prochazka
ac59665be4 formview - filtering by expanded columns 2021-01-24 08:37:32 +01:00
Jan Prochazka
df74dd114b v3.9.1-beta.1 2021-01-23 20:43:19 +01:00
Jan Prochazka
213ee01fa6 fix formview style 2021-01-23 20:40:49 +01:00
Jan Prochazka
e253cfb1b0 formview better style 2021-01-23 20:36:58 +01:00
Jan Prochazka
532c64840b Merge tag 'v3.9.0'
v3.9.0
2021-01-23 20:21:49 +01:00
Jan Prochazka
a23c882473 hints in references columns 2021-01-23 18:52:24 +01:00
Jan Prochazka
e4ad9acb68 formview - expand FK columns 2021-01-23 18:48:23 +01:00
123 changed files with 2553 additions and 717 deletions

33
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
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

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
.github/ISSUE_TEMPLATE/question.md vendored Normal file
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
.github/workflows/build-npm.yaml vendored Normal file
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
CHANGELOG.md Normal file
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

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)

View File

@@ -1,6 +1,6 @@
{
"name": "dbgate",
"version": "3.9.0",
"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"
}
}
}

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 =

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"

View File

@@ -1,5 +1,6 @@
{
"private": true,
"version": "3.9.6",
"name": "dbgate-all",
"workspaces": [
"packages/*"

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

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",

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;
},
};

View File

@@ -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') {

View File

@@ -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);
});

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 [];

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',
};

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;

View File

@@ -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' });
})

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 });

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'
};

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'),
};

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);
}

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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

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) {

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)) {

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);

View File

@@ -0,0 +1,9 @@
const { handleProcessCommunication } = require('../utility/processComm');
async function initializeApiEnvironment() {
process.on('message', async message => {
handleProcessCommunication(message);
});
}
module.exports = initializeApiEnvironment;

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);
}

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 };

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);
}

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);
});

View File

@@ -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;

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;
}

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;

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,
};

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,
};

View File

@@ -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,
};

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"
}

View File

@@ -41,7 +41,11 @@ export class FormViewDisplay {
...cfg.filters,
[column.uniqueName]: expr,
},
addedColumns: cfg.addedColumns.includes(column.uniqueName)
? cfg.addedColumns
: [...cfg.addedColumns, column.uniqueName],
}));
this.reload();
}
}

View File

@@ -32,10 +32,26 @@ export class TableFormViewDisplay extends FormViewDisplay {
) {
super(config, setConfig, cache, setCache, driver, dbinfo);
this.gridDisplay = new TableGridDisplay(tableName, driver, config, setConfig, cache, setCache, dbinfo);
this.gridDisplay.addAllExpandedColumnsToSelected = true;
this.isLoadedCorrectly = this.gridDisplay.isLoadedCorrectly && !!this.driver;
this.columns = this.gridDisplay.columns;
this.columns = [];
this.addDisplayColumns(this.gridDisplay.columns);
this.baseTable = this.gridDisplay.baseTable;
this.gridDisplay.hintBaseColumns = this.columns;
}
addDisplayColumns(columns: DisplayColumn[]) {
for (const col of columns) {
this.columns.push(col);
if (this.gridDisplay.isExpandedColumn(col.uniqueName)) {
const table = this.gridDisplay.getFkTarget(col);
if (table) {
const subcolumns = this.gridDisplay.getDisplayColumns(table, col.uniquePath);
this.addDisplayColumns(subcolumns);
}
}
}
}
getPrimaryKeyEqualCondition(row = null): Condition {
@@ -236,4 +252,13 @@ export class TableFormViewDisplay extends FormViewDisplay {
columnName: col.columnName,
};
}
toggleExpandedColumn(uniqueName: string) {
this.gridDisplay.toggleExpandedColumn(uniqueName);
this.gridDisplay.reload();
}
isExpandedColumn(uniqueName: string) {
return this.gridDisplay.isExpandedColumn(uniqueName);
}
}

View File

@@ -7,6 +7,8 @@ import { filterName } from './filterName';
export class TableGridDisplay extends GridDisplay {
public table: TableInfo;
public addAllExpandedColumnsToSelected = false;
public hintBaseColumns: DisplayColumn[];
constructor(
public tableName: NamedObjectInfo,
@@ -113,7 +115,7 @@ export class TableGridDisplay extends GridDisplay {
addHintsToSelect(select: Select): boolean {
let res = false;
const groupColumns = this.groupColumns;
for (const column of this.getGridColumns()) {
for (const column of this.hintBaseColumns || this.getGridColumns()) {
if (column.foreignKey) {
if (groupColumns && !groupColumns.includes(column.uniqueName)) {
continue;
@@ -205,7 +207,7 @@ export class TableGridDisplay extends GridDisplay {
displayedColumnInfo: DisplayedColumnInfo
) {
for (const column of columns) {
if (this.config.addedColumns.includes(column.uniqueName)) {
if (this.addAllExpandedColumnsToSelected || this.config.addedColumns.includes(column.uniqueName)) {
select.columns.push({
exprType: 'column',
columnName: column.columnName,

28
packages/dbgate/README.md Normal file
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
packages/dbgate/bin/dbgate.js Executable file
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');

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"
}
}

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"
}
}
}

View File

@@ -1,10 +1,11 @@
import { isTypeDateTime } from 'dbgate-tools';
import moment from 'moment';
export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends';
export function getFilterValueExpression(value, dataType) {
if (value == null) return 'NULL';
if (isTypeDateTime(dataType)) return value;
if (isTypeDateTime(dataType)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
return `="${value}"`;
}

View File

@@ -140,7 +140,7 @@ const yearMonthDayCondition = () => value => {
};
};
const fixedIntervalCondition = (start, end) => () => {
const createIntervalCondition = (start, end) => {
return {
conditionType: 'and',
conditions: [
@@ -157,7 +157,7 @@ const fixedIntervalCondition = (start, end) => () => {
},
{
conditionType: 'binary',
operator: '<',
operator: '<=',
left: {
exprType: 'placeholder',
},
@@ -170,13 +170,42 @@ const fixedIntervalCondition = (start, end) => () => {
};
};
const fixedMomentIntervalCondition = (intervalType, diff) => {
return fixedIntervalCondition(
moment().add(intervalType, diff).startOf(intervalType).toISOString(),
moment().add(intervalType, diff).endOf(intervalType).toISOString()
const createDateIntervalCondition = (start, end) => {
return createIntervalCondition(start.format('YYYY-MM-DDTHH:mm:ss.SSS'), end.format('YYYY-MM-DDTHH:mm:ss.SSS'));
};
const fixedMomentIntervalCondition = (intervalType, diff) => () => {
return createDateIntervalCondition(
moment().add(intervalType, diff).startOf(intervalType),
moment().add(intervalType, diff).endOf(intervalType)
);
};
const yearMonthDayMinuteCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
const year = m[1];
const month = m[2];
const day = m[3];
const hour = m[4];
const minute = m[5];
const dateObject = new Date(year, month - 1, day, hour, minute);
return createDateIntervalCondition(moment(dateObject).startOf('minute'), moment(dateObject).endOf('minute'));
};
const yearMonthDaySecondCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
const year = m[1];
const month = m[2];
const day = m[3];
const hour = m[5];
const minute = m[6];
const second = m[7];
const dateObject = new Date(year, month - 1, day, hour, minute, second);
return createDateIntervalCondition(moment(dateObject).startOf('second'), moment(dateObject).endOf('second'));
};
const createParser = (filterType: FilterType) => {
const langDef = {
string1: () =>
@@ -209,6 +238,8 @@ const createParser = (filterType: FilterType) => {
yearNum: () => P.regexp(/\d\d\d\d/).map(yearCondition()),
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
yearMonthDayMinute: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteCondition()),
yearMonthDaySecond: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
value: r => P.alt(...allowedValues.map(x => r[x])),
valueTestEq: r => r.value.map(binaryCondition('=')),
@@ -286,6 +317,8 @@ const createParser = (filterType: FilterType) => {
if (filterType == 'logical') allowedElements.push('true', 'false', 'trueNum', 'falseNum');
if (filterType == 'datetime')
allowedElements.push(
'yearMonthDaySecond',
'yearMonthDayMinute',
'yearMonthDayNum',
'yearMonthNum',
'yearNum',

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

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"
}
}
}

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"
}
}
}

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);
}
}

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"
}
}

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"
}
}
}

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>

View File

@@ -16,7 +16,7 @@ import DragAndDropFileTarget from './DragAndDropFileTarget';
import { useUploadsZone } from './utility/UploadsProvider';
import useTheme from './theme/useTheme';
import { MenuLayer } from './modals/showMenu';
import ErrorBoundary from './utility/ErrorBoundary';
import ErrorBoundary, { ErrorBoundaryTest } from './utility/ErrorBoundary';
const BodyDiv = styled.div`
position: fixed;
@@ -100,45 +100,48 @@ 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();
return (
<div {...getRootProps()}>
<ToolBarDiv theme={theme}>
<ToolBar toolbarPortalRef={toolbarPortalRef} />
</ToolBarDiv>
<IconBar theme={theme}>
<WidgetIconPanel />
</IconBar>
{!!currentWidget && (
<LeftPanel theme={theme}>
<ErrorBoundary>
<WidgetContainer />
</ErrorBoundary>
</LeftPanel>
)}
{!!currentWidget && (
<ScreenHorizontalSplitHandle
onMouseDown={onSplitDown}
theme={theme}
style={{ left: leftPanelWidth + dimensions.widgetMenu.iconSize }}
/>
)}
<TabsPanelContainer contentLeft={contentLeft} theme={theme}>
<TabsPanel></TabsPanel>
</TabsPanelContainer>
<BodyDiv contentLeft={contentLeft} theme={theme}>
<TabContent toolbarPortalRef={toolbarPortalRef} />
</BodyDiv>
<StausBarContainer theme={theme}>
<StatusBar />
</StausBarContainer>
<ModalLayer />
<MenuLayer />
<ErrorBoundary>
<ToolBarDiv theme={theme}>
<ToolBar toolbarPortalRef={toolbarPortalRef} />
</ToolBarDiv>
<IconBar theme={theme}>
<WidgetIconPanel />
</IconBar>
{!!currentWidget && (
<LeftPanel theme={theme}>
<ErrorBoundary>
<WidgetContainer />
</ErrorBoundary>
</LeftPanel>
)}
{!!currentWidget && (
<ScreenHorizontalSplitHandle
onMouseDown={onSplitDown}
theme={theme}
style={{ left: leftPanelWidth + dimensions.widgetMenu.iconSize }}
/>
)}
<TabsPanelContainer contentLeft={contentLeft} theme={theme}>
<TabsPanel></TabsPanel>
</TabsPanelContainer>
<BodyDiv contentLeft={contentLeft} theme={theme}>
<TabContent toolbarPortalRef={toolbarPortalRef} statusbarPortalRef={statusbarPortalRef} />
</BodyDiv>
<StausBarContainer theme={theme}>
<StatusBar statusbarPortalRef={statusbarPortalRef} />
</StausBarContainer>
<ModalLayer />
<MenuLayer />
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
</ErrorBoundary>
</div>
);
}

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,33 +48,37 @@ function createTabComponent(selectedTab) {
return null;
}
export default function TabContent({ toolbarPortalRef }) {
export default function TabContent({ toolbarPortalRef, statusbarPortalRef }) {
const files = useOpenedTabs();
const [mountedTabs, setMountedTabs] = React.useState({});
// cleanup closed tabs
if (
_.difference(
_.keys(mountedTabs),
_.map(
files.filter(x => x.closedTime == null),
'tabid'
)
).length > 0
) {
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find(x => x.tabid == k && x.closedTime == null)));
}
const selectedTab = files.find(x => x.selected && x.closedTime == null);
const selectedTab = files.find(x => x.selected);
if (selectedTab) {
const { tabid } = selectedTab;
if (tabid && !mountedTabs[tabid])
setMountedTabs({
...mountedTabs,
[tabid]: createTabComponent(selectedTab),
});
}
React.useEffect(() => {
// cleanup closed tabs
if (
_.difference(
_.keys(mountedTabs),
_.map(
files.filter(x => x.closedTime == null),
'tabid'
)
).length > 0
) {
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find(x => x.tabid == k && x.closedTime == null)));
}
if (selectedTab) {
const { tabid } = selectedTab;
if (tabid && !mountedTabs[tabid])
setMountedTabs({
...mountedTabs,
[tabid]: createTabComponent(selectedTab),
});
}
}, [mountedTabs, files]);
return _.keys(mountedTabs).map(tabid => {
const { TabComponent, props } = mountedTabs[tabid];
@@ -80,6 +90,7 @@ export default function TabContent({ toolbarPortalRef }) {
tabid={tabid}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
statusbarPortalRef={statusbarPortalRef}
TabComponent={TabComponent}
/>
);

View File

@@ -9,6 +9,8 @@ import { FontIcon } from './icons';
import useTheme from './theme/useTheme';
import usePropsCompare from './utility/usePropsCompare';
import { useShowMenu } from './modals/showMenu';
import { setSelectedTabFunc } from './utility/common';
import getElectron from './utility/getElectron';
// const files = [
// { name: 'app.js' },
@@ -123,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();
@@ -140,40 +151,28 @@ export default function TabsPanel() {
if (e.target.closest('.tabCloseButton')) {
return;
}
setOpenedTabs(files =>
files.map(x => ({
...x,
selected: x.tabid == tabid,
}))
);
setOpenedTabs(files => setSelectedTabFunc(files, tabid));
};
const closeTabFunc = closeCondition => tabid => {
setOpenedTabs(files => {
const active = files.find(x => x.tabid == tabid);
if (!active) return files;
const lastSelectedIndex = _.findIndex(files, x => x.tabid == tabid);
let selectedIndex = lastSelectedIndex;
const newFiles = files.map(x => ({
...x,
closedTime: x.closedTime || (closeCondition(x, active) ? new Date().getTime() : undefined),
}));
while (selectedIndex >= 0 && newFiles[selectedIndex].closedTime) selectedIndex -= 1;
if (selectedIndex < 0) {
selectedIndex = lastSelectedIndex;
while (selectedIndex < newFiles.length && newFiles[selectedIndex].closedTime) selectedIndex += 1;
if (newFiles.find(x => x.selected && x.closedTime == null)) {
return newFiles;
}
if (selectedIndex != lastSelectedIndex) {
return newFiles.map((x, index) => ({
...x,
selected: index == selectedIndex,
}));
}
const selectedIndex = _.findLastIndex(newFiles, x => x.closedTime == null);
return newFiles;
return newFiles.map((x, index) => ({
...x,
selected: index == selectedIndex,
}));
});
};
@@ -221,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)
@@ -260,10 +269,10 @@ export default function TabsPanel() {
<FontIcon icon={getDbIcon(dbKey)} /> {tabsByDb[dbKey][0].tabDbName}
</DbNameWrapper>
<DbGroupHandler>
{_.sortBy(tabsByDb[dbKey], 'title').map(tab => (
{_.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)}

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}
</>
);
}

View File

@@ -4,6 +4,15 @@ import moment from 'moment';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { useSetOpenedTabs } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore';
import { setSelectedTabFunc } from '../utility/common';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
const InfoDiv = styled.div`
margin-left: 30px;
color: ${props => props.theme.left_font3};
`;
function Menu({ data }) {
const setOpenedTabs = useSetOpenedTabs();
@@ -24,14 +33,17 @@ 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 =>
files.map(x => ({
...x,
selected: x.tabid == tabid,
closedTime: x.tabid == tabid ? undefined : x.closedTime,
}))
setSelectedTabFunc(
files.map(x => ({
...x,
closedTime: x.tabid == tabid ? undefined : x.closedTime,
})),
tabid
)
);
};
@@ -45,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>
);
}

View File

@@ -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;

View File

@@ -20,7 +20,7 @@ function Menu({ data }) {
const handleNewQuery = () => {
openNewTab({
title: 'Query',
title: 'Query #',
icon: 'img sql-file',
tooltip,
tabComponent: 'QueryTab',

View File

@@ -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: {

View File

@@ -104,7 +104,7 @@ export function SavedSqlFileAppObject({ data, commonProps }) {
openNewTab(
{
title: 'Shell',
title: 'Shell #',
icon: 'img shell',
tabComponent: 'ShellTab',
},

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>

View File

@@ -9,6 +9,7 @@ import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject';
import { useSetOpenedTabs } from '../utility/globalState';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import useOpenNewTab from '../utility/useOpenNewTab';
const HeaderDiv = styled.div`
display: flex;
@@ -52,11 +53,11 @@ export default function ColumnHeaderControl({
}) {
const onResizeDown = useSplitterDrag('clientX', onResize);
const { foreignKey } = column;
const setOpenedTabs = useSetOpenedTabs();
const openNewTab = useOpenNewTab();
const theme = useTheme();
const openReferencedTable = () => {
openDatabaseObjectDetail(setOpenedTabs, 'TableDataTab', null, {
openDatabaseObjectDetail(openNewTab, 'TableDataTab', null, {
schemaName: foreignKey.refSchemaName,
pureName: foreignKey.refTableName,
conid,

View File

@@ -3,11 +3,17 @@
import React from 'react';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
const Label = styled.span`
font-weight: ${props => (props.notNull ? 'bold' : 'normal')};
white-space: nowrap;
`;
const ExtInfoWrap = styled.span`
font-weight: normal;
margin-left: 5px;
color: ${props => props.theme.left_font3};
`;
export function getColumnIcon(column, forceIcon = false) {
if (column.autoIncrement) return 'img autoincrement';
@@ -19,9 +25,11 @@ export function getColumnIcon(column, forceIcon = false) {
/** @param column {import('dbgate-datalib').DisplayColumn|import('dbgate-types').ColumnInfo} */
export default function ColumnLabel(column) {
const icon = getColumnIcon(column, column.forceIcon);
const theme = useTheme();
return (
<Label {...column}>
{icon ? <FontIcon icon={icon} /> : null} {column.headerText || column.columnName}
{column.extInfo ? <ExtInfoWrap theme={theme}>{column.extInfo}</ExtInfoWrap> : null}
</Label>
);
}

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: {},

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: {

View File

@@ -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>

View File

@@ -16,7 +16,7 @@ import { CellFormattedValue, ShowFormButton } from '../datagrid/DataGridRow';
import { cellFromEvent } from '../datagrid/selection';
import InplaceEditor from '../datagrid/InplaceEditor';
import { copyTextToClipboard } from '../utility/clipboard';
import { FontIcon } from '../icons';
import { ExpandIcon, FontIcon } from '../icons';
import openReferenceForm from './openReferenceForm';
import useOpenNewTab from '../utility/useOpenNewTab';
import LoadingInfo from '../widgets/LoadingInfo';
@@ -26,6 +26,14 @@ const Table = styled.table`
outline: none;
`;
const OuterWrapper = styled.div`
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
`;
const Wrapper = styled.div`
position: absolute;
left: 0;
@@ -107,6 +115,10 @@ const HintSpan = styled.span`
margin-right: 16px;
`;
const ColumnLabelMargin = styled(ColumnLabel)`
margin-right: 16px;
`;
function isDataCell(cell) {
return cell[1] % 2 == 1;
}
@@ -416,13 +428,18 @@ export default function FormView(props) {
const [inplaceEditorState, dispatchInsplaceEditor] = React.useReducer((state, action) => {
switch (action.type) {
case 'show':
case 'show': {
const column = getCellColumn(action.cell);
if (!column) return state;
if (column.uniquePath.length > 1) return state;
// if (!grider.editable) return {};
return {
cell: action.cell,
text: action.text,
selectAll: action.selectAll,
};
}
case 'close': {
const [row, col] = currentCell || [];
if (focusFieldRef.current) focusFieldRef.current.focus();
@@ -468,79 +485,99 @@ export default function FormView(props) {
if (!formDisplay || !formDisplay.isLoadedCorrectly) return toolbar;
return (
<Wrapper ref={wrapperRef} onContextMenu={handleContextMenu}>
{columnChunks.map((chunk, chunkIndex) => (
<Table key={chunkIndex} onMouseDown={handleTableMouseDown}>
{chunk.map((col, rowIndex) => (
<TableRow key={col.columnName} theme={theme} ref={headerRowRef} style={{ height: `${rowHeight}px` }}>
<TableHeaderCell
theme={theme}
data-row={rowIndex}
data-col={chunkIndex * 2}
// @ts-ignore
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2}
ref={element => setCellRef(rowIndex, chunkIndex * 2, element)}
>
<ColumnLabel {...col} />
</TableHeaderCell>
<TableBodyCell
theme={theme}
data-row={rowIndex}
data-col={chunkIndex * 2 + 1}
// @ts-ignore
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1}
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
ref={element => setCellRef(rowIndex, chunkIndex * 2 + 1, element)}
>
{inplaceEditorState.cell &&
rowIndex == inplaceEditorState.cell[0] &&
chunkIndex * 2 + 1 == inplaceEditorState.cell[1] ? (
<InplaceEditor
widthPx={getCellWidth(rowIndex, chunkIndex * 2 + 1)}
inplaceEditorState={inplaceEditorState}
dispatchInsplaceEditor={dispatchInsplaceEditor}
cellValue={rowData[col.uniqueName]}
onSetValue={value => {
former.setCellValue(col.uniqueName, value);
}}
// grider={grider}
// rowIndex={rowIndex}
// uniqueName={col.uniqueName}
<OuterWrapper>
<Wrapper ref={wrapperRef} onContextMenu={handleContextMenu}>
{columnChunks.map((chunk, chunkIndex) => (
<Table key={chunkIndex} onMouseDown={handleTableMouseDown}>
{chunk.map((col, rowIndex) => (
<TableRow key={col.uniqueName} theme={theme} ref={headerRowRef} style={{ height: `${rowHeight}px` }}>
<TableHeaderCell
theme={theme}
data-row={rowIndex}
data-col={chunkIndex * 2}
// @ts-ignore
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2}
ref={element => setCellRef(rowIndex, chunkIndex * 2, element)}
>
<ColumnLabelMargin
{...col}
headerText={col.columnName}
style={{ marginLeft: (col.uniquePath.length - 1) * 20 }}
extInfo={col.foreignKey ? ` -> ${col.foreignKey.refTableName}` : null}
/>
) : (
<>
{rowData && (
<CellFormattedValue value={rowData[col.columnName]} dataType={col.dataType} theme={theme} />
)}
{!!col.hintColumnName &&
rowData &&
!(rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) && (
<HintSpan>{rowData[col.hintColumnName]}</HintSpan>
{col.foreignKey && (
<ShowFormButton
theme={theme}
className="buttonLike"
onClick={e => {
e.stopPropagation();
formDisplay.toggleExpandedColumn(col.uniqueName);
}}
>
<ExpandIcon isExpanded={formDisplay.isExpandedColumn(col.uniqueName)} />
</ShowFormButton>
)}
</TableHeaderCell>
<TableBodyCell
theme={theme}
data-row={rowIndex}
data-col={chunkIndex * 2 + 1}
// @ts-ignore
isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2 + 1}
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
ref={element => setCellRef(rowIndex, chunkIndex * 2 + 1, element)}
>
{inplaceEditorState.cell &&
rowIndex == inplaceEditorState.cell[0] &&
chunkIndex * 2 + 1 == inplaceEditorState.cell[1] ? (
<InplaceEditor
widthPx={getCellWidth(rowIndex, chunkIndex * 2 + 1)}
inplaceEditorState={inplaceEditorState}
dispatchInsplaceEditor={dispatchInsplaceEditor}
cellValue={rowData[col.uniqueName]}
onSetValue={value => {
former.setCellValue(col.uniqueName, value);
}}
// grider={grider}
// rowIndex={rowIndex}
// uniqueName={col.uniqueName}
/>
) : (
<>
{rowData && (
<CellFormattedValue value={rowData[col.uniqueName]} dataType={col.dataType} theme={theme} />
)}
{col.foreignKey && rowData && rowData[col.uniqueName] && (
<ShowFormButton
theme={theme}
className="buttonLike"
onClick={e => {
e.stopPropagation();
openReferenceForm(rowData, col, openNewTab, conid, database);
}}
>
<FontIcon icon="icon form" />
</ShowFormButton>
)}
</>
)}
</TableBodyCell>
</TableRow>
))}
</Table>
))}
{!!col.hintColumnName &&
rowData &&
!(rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) && (
<HintSpan>{rowData[col.hintColumnName]}</HintSpan>
)}
{col.foreignKey && rowData && rowData[col.uniqueName] && (
<ShowFormButton
theme={theme}
className="buttonLike"
onClick={e => {
e.stopPropagation();
openReferenceForm(rowData, col, openNewTab, conid, database);
}}
>
<FontIcon icon="icon form" />
</ShowFormButton>
)}
</>
)}
</TableBodyCell>
</TableRow>
))}
</Table>
))}
<FocusField type="text" ref={focusFieldRef} onKeyDown={handleKeyDown} />
<FocusField type="text" ref={focusFieldRef} onKeyDown={handleKeyDown} />
{toolbar}
</Wrapper>
{rowCountInfo && <RowCountLabel theme={theme}>{rowCountInfo}</RowCountLabel>}
{toolbar}
</Wrapper>
</OuterWrapper>
);
}

View File

@@ -86,17 +86,18 @@ export default function FormViewFilters(props) {
{baseTable.primaryKey.columns.map(col => (
<PrimaryKeyFilterEditor key={col.columnName} baseTable={baseTable} column={col} formDisplay={formDisplay} />
))}
{allFilterNames.map(columnName => {
const column = baseTable.columns.find(x => x.columnName == columnName);
{allFilterNames.map(uniqueName => {
const column = formDisplay.columns.find(x => x.uniqueName == uniqueName)
// const column = baseTable.columns.find(x => x.columnName == columnName);
if (!column) return null;
return (
<ColumnWrapper key={columnName}>
<ColumnWrapper key={uniqueName}>
<ColumnNameWrapper>
<ColumnLabel {...column} />
<InlineButton
square
onClick={() => {
formDisplay.removeFilter(column.columnName);
formDisplay.removeFilter(column.uniqueName);
}}
>
<FontIcon icon="icon delete" />
@@ -104,8 +105,8 @@ export default function FormViewFilters(props) {
</ColumnNameWrapper>
<DataFilterControl
filterType={getFilterType(column.dataType)}
filter={filters[column.columnName]}
setFilter={value => formDisplay.setFilter(column.columnName, value)}
filter={filters[column.uniqueName]}
setFilter={value => formDisplay.setFilter(column.uniqueName, value)}
/>
</ColumnWrapper>
);

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;

View File

@@ -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,

View File

@@ -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 =

View File

@@ -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,

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;
}

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'));

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>

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">

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>

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>

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>
);
}

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>

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>
)}
</>
);
}

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>

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>
)}
</>
);
}

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',

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

View File

@@ -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>
)}
</>
);
}

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>
</>
);
}

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} />
</>
);
}

View File

@@ -10,10 +10,11 @@ import useModalState from '../modals/useModalState';
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();
@@ -29,12 +30,7 @@ export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef,
await saveToStorage();
const existing = (openedTabs || []).find(x => x.props && x.props.sourceTabId == tabid && x.closedTime == null);
if (existing) {
setOpenedTabs(tabs =>
tabs.map(x => ({
...x,
selected: x.tabid == existing.tabid,
}))
);
setOpenedTabs(tabs => setSelectedTabFunc(tabs, existing.tabid));
} else {
const thisTab = (openedTabs || []).find(x => x.tabid == tabid);
openNewTab({
@@ -65,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"
/>
</>
);

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"
/>
</>
);

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"
/>
</>
);

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"
/>
</>
);

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};

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