1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-06-05 21:59:22 +02:00

Compare commits

...

165 Commits

Author SHA1 Message Date
14d5842056 chore(release): 0.2.1 2021-07-09 16:07:07 +02:00
f19f9e23a2 refactor(UI): changed buttons icon position 2021-07-09 15:51:02 +02:00
0252a064d9 feat(UI): contextual menu shortcuts to create new elements on folders 2021-07-09 15:12:16 +02:00
439356a019 feat: context menu option to duplicate connections 2021-07-09 11:18:40 +02:00
c6897af22d feat(MySQL): possibility to set a default schema in connection parameters 2021-07-09 10:26:16 +02:00
7e40dbfba3 Merge pull request #84 from Fabio286/new-commection-mode
feat: new connection mode
2021-07-08 18:50:23 +02:00
27a153ef43 refactor(UI): improved new connection panels appearence 2021-07-08 18:43:31 +02:00
56fcc2650b fix: clear corrupted configurations to avoid exceptions 2021-07-08 18:42:19 +02:00
9622dbec6b chore: update rules 2021-07-08 18:41:17 +02:00
a0ab63bdb5 fix(UI): connection tab indicator when scrolling 2021-07-08 17:58:43 +02:00
8cd76e711c feat(UI): new connection add panel 2021-07-08 17:43:33 +02:00
9af71a6e34 feat(UI): new connection edit panel 2021-07-08 15:06:20 +02:00
e7b3c28826 chore: update README.md 2021-07-07 12:59:27 +02:00
7570b0add8 fix: avoid to trigger schema loading multiple times 2021-07-06 09:36:35 +02:00
Christian Ratz
1801bef019 feat: SSH Tunnel functionality (#81)
* added ssh-tunnel-functionality for mysql-connections

* remove autoformat-stuff

* added identity for using ssh-key

* added identity to mysqlclient to use sshkey

* removed debug console.log

* added ssh-tunnel-functionality for postgresqlclient

* changed naming to sshKey for sshKey-input

* refactoring code

* fix lint

* set dbConfig.ssl to null initially
2021-07-05 09:30:52 +02:00
0db5ebd7bf chore: update README logo 2021-07-03 13:47:37 +02:00
5c21b67b3d chore(release): 0.2.0 2021-07-03 12:29:13 +02:00
08d5b1b329 feat: contextual option to duplicate tables 2021-07-03 12:27:50 +02:00
61a42d51f5 fix(UI): contextual sub-menu alignment when close to the lower edge of the window 2021-07-03 12:01:19 +02:00
d96907ca2d fix(PostgreSQL): error opening setting tab for some stored routines 2021-07-03 11:39:57 +02:00
75bbd5f66e feat(PostgreSQL): trigger functions support 2021-07-03 11:29:14 +02:00
faa07a077c fix: fields default not correctly set in table filler 2021-07-01 19:18:57 +02:00
7ca69e51ae refactor(UI): round corners to editor autocomplete 2021-06-30 19:43:30 +02:00
d868c772b9 feat(UI): context option to copy cell or row value 2021-06-30 19:27:24 +02:00
a69bdeb20d feat(UI): resizer border mouse hover animation 2021-06-30 09:49:32 +02:00
51db2795bb refactor(UI): minor UI improvements 2021-06-29 23:31:18 +02:00
2d278aa14e fix: rows loses internal id after export 2021-06-29 23:23:11 +02:00
f74bca7bb4 perf: improved contextual menu appearance 2021-06-29 18:22:40 +02:00
978a7c5f5b feat(UI): active tab animation 2021-06-28 18:34:39 +02:00
0110756204 fix(UI): editor blinking on first connection with a dark theme 2021-06-27 12:28:55 +02:00
7df0cf8389 feat(PostgreSQL): triggers creation 2021-06-26 16:36:05 +02:00
3aef7e953e fix: vulnerability in server error toast messages 2021-06-25 09:35:20 +02:00
a975df38dd fix: unhandled exception in connection test 2021-06-25 09:29:59 +02:00
a0a025e450 perf(UI): increased application border-radius 2021-06-24 21:49:46 +02:00
7cc5e7b67f chore(release): 0.1.13 2021-06-19 13:04:11 +02:00
e2ebb04a90 feat(UI): ability to manually insert page number in DATA tabs, closes #65 2021-06-19 12:13:09 +02:00
e579f37438 feat(UI): option to change query editors font size, closes #77 2021-06-19 11:54:15 +02:00
3829b94bf7 refactor: solved deprecation warning for url.format() 2021-06-18 21:08:21 +02:00
5c8ee66f43 feat(PostgreSQL): alter trigger support 2021-06-17 22:01:18 +02:00
b77e77f3bb chore: update dependencies 2021-06-14 09:11:37 +02:00
1d5614278c ci: moved to node 14 in build actions 2021-06-14 09:11:23 +02:00
8a20befd09 fix(UI): various fixes in displaying content with small window size 2021-06-13 11:16:21 +02:00
4133fc452f fix(MySQL): pool connections not released after MySQL errors, causing endless load animation 2021-06-11 15:02:45 +02:00
690a4541f9 perf: remove comments from queries before execution 2021-06-11 14:32:51 +02:00
71910ca5c8 chore(release): 0.1.12 2021-06-09 09:03:18 +02:00
cce5adbac7 feat(PostgreSQL): trigger rename and delete 2021-06-08 09:12:43 +02:00
e6d67fcb0c Update README.md 2021-06-07 23:03:08 +02:00
e83b80afc9 chore: update README.md 2021-06-05 12:27:20 +02:00
8742fa10f0 fix: internal exceptions 2021-06-05 10:15:44 +02:00
ab54eb086f chore: update dependencies 2021-06-05 10:15:08 +02:00
308f69f12e chore: update README.md 2021-06-04 14:01:46 +02:00
2f30bafa33 Merge pull request #74 from digitalgopnik/master
feat: added translation for de-DE
2021-06-04 10:39:22 +02:00
Christian
1716077182 added translation for de-DE 2021-06-04 09:37:02 +02:00
5562e73e75 fix: page offset not reset when selected table changes 2021-06-03 11:02:24 +02:00
8a7cc2a14f fix(UI): unable to browse view's result pages 2021-06-03 10:57:57 +02:00
9ca059d979 fix(MySQL): view's data tab doesn't work with some views, closes #71 2021-06-03 10:54:59 +02:00
c9ccf786cd chore(release): 0.1.11 2021-06-02 15:47:12 +02:00
5e9c88a7fd fix(UI): prevent canc key to trigger delete modal when editing a row 2021-06-02 12:32:12 +02:00
e0e6483d1f build: update dependencies 2021-06-02 12:13:43 +02:00
66227569f4 fix: table row loses internal id after cell update 2021-06-02 11:58:34 +02:00
faa799c8ea fix(MySQL): missing schema altering tables in some conditions 2021-05-31 17:07:48 +02:00
acc1eeb094 fix: empty offset in cell update queries 2021-05-31 14:27:02 +02:00
865dad80df chore: update README.md 2021-05-30 10:54:58 +02:00
f502620779 chore(release): 0.1.10 2021-05-29 11:41:48 +02:00
f61d7eeaf4 feat: key shortcuts to change DATA tab page 2021-05-29 11:26:23 +02:00
e71c7568c0 feat: option to set DATA tab page size 2021-05-29 11:04:02 +02:00
79f033e524 feat: prev/next buttons to browse the results pages of data tab 2021-05-27 22:13:59 +02:00
310cfaa3c2 fix(MySQL): wrong schema in view data tab select, closes #71 2021-05-26 17:44:33 +02:00
6f93e1f9ab chore: AUR badge on README.md 2021-05-26 13:19:48 +02:00
04bdd085a5 fix: wrong detection of field default expressions in some cases 2021-05-25 16:54:13 +02:00
c568acae14 build: update dependencies 2021-05-25 10:09:09 +02:00
7a46a119d8 chore(release): 0.1.9 2021-05-23 11:35:54 +02:00
f1636f1528 fix(MySQL): can't access tables having UNIQUE KEY, closes #69 2021-05-23 11:12:09 +02:00
f839d5cd3c chore: Twitter link in about tab 2021-05-23 10:19:56 +02:00
8f4f1d393c chore: update README.md 2021-05-22 17:33:03 +02:00
ab17c6d4bf chore: update README.md 2021-05-22 17:26:32 +02:00
b4b2f3d0cf chore(release): 0.1.8 2021-05-22 16:25:52 +02:00
7a766f04e6 perf: improved the way how field default value are handled 2021-05-22 16:24:19 +02:00
f6b77d1243 build: update dependencies 2021-05-22 16:12:08 +02:00
840241c3cc refactor: better description in foreign key value select 2021-05-20 15:07:51 +02:00
0f10c9e824 fix: internal id not removed before row update 2021-05-20 12:45:49 +02:00
1e37f2a96f fix: unable to add new ENUM fields 2021-05-20 12:38:05 +02:00
0d6137195d fix: unable to delete table rows 2021-05-19 16:55:38 +02:00
d26f168250 build: update build script 2021-05-17 19:08:36 +02:00
5efcad4d3f chore(release): 0.1.7 2021-05-16 21:08:52 +02:00
0a872e7023 build: separate build configurations 2021-05-16 21:03:04 +02:00
0e425a9c6d build: removed linux arm64 build from build process 2021-05-16 19:50:06 +02:00
434711a360 perf(MySQL): improved connections pool handling 2021-05-15 21:47:30 +02:00
854472c7a3 fix: row loses internal id after cell update 2021-05-15 19:33:31 +02:00
9a7cd90d5c build: arm mac (apple silicon) build configuration 2021-05-14 22:24:50 +02:00
026400d242 build: arm linux build configuration 2021-05-14 18:59:58 +02:00
4bd8cbbf6c build: moved to sass from node-sass 2021-05-14 17:27:56 +02:00
1382bc9300 Merge pull request #64 from Fabio286/dependabot/npm_and_yarn/pgsql-ast-parser-8.1.1
build(deps): bump pgsql-ast-parser from 7.2.1 to 8.1.1
2021-05-14 17:21:10 +02:00
ae103e5477 perf(core): increased connection pool size to improve performance 2021-05-14 17:17:37 +02:00
6b0b8b19d7 fix(MySQL): connection loses schema in some conditions 2021-05-14 17:02:27 +02:00
475397ca34 fix: issue with ENUM and SET fields on table filler modal 2021-05-13 20:46:44 +02:00
7a62131cc7 fix: issue with ENUM and SET length when creating a new field 2021-05-13 15:45:54 +02:00
c7663be338 fix: multiple row select on sorted tables not work properly 2021-05-11 18:32:11 +02:00
496490b14a build: update dependencies and build config 2021-05-11 12:00:22 +02:00
dependabot[bot]
672573d8c2 build(deps): bump pgsql-ast-parser from 7.2.1 to 8.1.1
Bumps [pgsql-ast-parser](https://github.com/oguimbal/pgsql-ast-parser) from 7.2.1 to 8.1.1.
- [Release notes](https://github.com/oguimbal/pgsql-ast-parser/releases)
- [Changelog](https://github.com/oguimbal/pgsql-ast-parser/blob/master/changelog.md)
- [Commits](https://github.com/oguimbal/pgsql-ast-parser/compare/7.2.1...8.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 06:16:49 +00:00
d923453919 chore(release): 0.1.6 2021-05-08 17:33:25 +02:00
0435279604 build: temporarily disabled Windows builds due issues with dependencies 2021-05-08 17:22:45 +02:00
79cef61fea build: revert to electron-builder@22.9.1 due Windows build issues 2021-05-08 17:13:24 +02:00
dec334737f chore: update dependencies 2021-05-08 16:41:18 +02:00
84c3692e95 chore: icons for appx build 2021-05-08 16:04:37 +02:00
5dc0b0bea4 perf: italian translation updated 2021-05-07 12:11:55 +02:00
19d16e46bb build: update dependencies 2021-05-06 22:55:10 +02:00
3baf6fa173 fix: better detection and handling of field default type 2021-05-06 22:21:42 +02:00
29e2d92b5b fix: no quotes around strings in field default custom value 2021-05-05 17:13:12 +02:00
bebba64d06 feat(MySQL): ENUM and SET fields support, closes #61 2021-05-04 21:50:41 +02:00
9dfe7cca22 fix(UI): data type figure twice on type select 2021-05-03 22:55:39 +02:00
35ef070725 fix: support to mDNS/zeroconf in snap build, closes #58 2021-05-01 12:16:57 +02:00
370ad6a536 chore(release): 0.1.5 2021-04-30 17:37:04 +02:00
5822b3df43 perf(UI): new application icon 2021-04-30 14:14:01 +02:00
5208ec171b fix(MySQL): multiple queries non properly split in some cases 2021-04-29 21:03:32 +02:00
6e332da425 chore: update issue template 2021-04-28 16:56:04 +02:00
bf3367b41d build: minor changes to build and dependencies 2021-04-28 12:11:39 +02:00
ecfb732c26 fix: % character not properly escaped, closes #60 2021-04-28 12:10:43 +02:00
fd4f032a6f build: mssql temporarily removed from dependencies 2021-04-28 11:55:29 +02:00
1b09909126 fix: semicolon inside strings breaks queries, closes #59 2021-04-28 11:50:07 +02:00
04cd806954 chore: update README.md 2021-04-26 10:16:55 +02:00
773cb36ca1 Merge pull request #57 from Fabio286/dependabot/npm_and_yarn/electron-store-8.0.0
build(deps): bump electron-store from 7.0.3 to 8.0.0
2021-04-26 10:12:32 +02:00
ccacd3e2c3 Merge pull request #56 from Fabio286/dependabot/npm_and_yarn/electron-12.0.5
build(deps-dev): bump electron from 11.4.3 to 12.0.5
2021-04-26 10:08:32 +02:00
15948b30c9 refactor: modifications for electron 12 support 2021-04-26 10:07:47 +02:00
3dcb5d4f14 Merge pull request #55 from Fabio286/dependabot/npm_and_yarn/stylelint-config-standard-22.0.0
build(deps-dev): bump stylelint-config-standard from 21.0.0 to 22.0.0
2021-04-26 08:59:35 +02:00
dependabot[bot]
64d93d7c40 build(deps): bump electron-store from 7.0.3 to 8.0.0
Bumps [electron-store](https://github.com/sindresorhus/electron-store) from 7.0.3 to 8.0.0.
- [Release notes](https://github.com/sindresorhus/electron-store/releases)
- [Commits](https://github.com/sindresorhus/electron-store/compare/v7.0.3...v8.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-26 06:26:31 +00:00
dependabot[bot]
2d85295093 build(deps-dev): bump electron from 11.4.3 to 12.0.5
Bumps [electron](https://github.com/electron/electron) from 11.4.3 to 12.0.5.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v11.4.3...v12.0.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-26 06:25:55 +00:00
dependabot[bot]
cd58e2d8ca build(deps-dev): bump stylelint-config-standard from 21.0.0 to 22.0.0
Bumps [stylelint-config-standard](https://github.com/stylelint/stylelint-config-standard) from 21.0.0 to 22.0.0.
- [Release notes](https://github.com/stylelint/stylelint-config-standard/releases)
- [Changelog](https://github.com/stylelint/stylelint-config-standard/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint-config-standard/compare/21.0.0...22.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-26 06:25:16 +00:00
d87495822e Merge pull request #54 from daeleduardo/master
feat: portugues (Brasil) translation
2021-04-25 11:44:55 +02:00
Daniel Aguiar
8720bcdad6 New Translation: Portugues (Brasil) 2021-04-25 03:53:21 -03:00
1df57dc705 chore(release): 0.1.4 2021-04-22 15:18:07 +02:00
86240fb53c refactor(PostgreSQL): preparing code to support triggers 2021-04-22 15:15:08 +02:00
1d363f755e feat: query results export 2021-04-22 15:08:22 +02:00
0d77aee3eb refactor: Improved pulse animation code 2021-04-22 14:24:34 +02:00
a41cf1ab56 fix: wrong changelog in some cases 2021-04-22 11:35:59 +02:00
5ceddb8e00 perf(UI): improved connection status indicator 2021-04-21 16:41:42 +02:00
16e17b39b6 feat(UI): ctrl+s shortcut to save changes 2021-04-20 17:39:15 +02:00
20cba6ee9b feat(UI): canc press to delete selected rows 2021-04-20 16:30:10 +02:00
9ffd443a66 feat(UI): format and clear queries 2021-04-19 19:15:06 +02:00
f82dbd24dc fix: launch from shortcut of procedures or functions with parameters without name dont works 2021-04-19 15:40:25 +02:00
6eb2977568 fix(UI): data type not listed in selection if not present in global types 2021-04-19 11:07:29 +02:00
cafb65560a chore: update README.md 2021-04-17 12:28:55 +02:00
532d963019 chore: update README.md 2021-04-17 11:28:02 +02:00
1b0a63ff31 chore(release): 0.1.3 2021-04-17 10:36:07 +02:00
c22187c305 perf(UI): improved table fields suggestion in query editor 2021-04-17 10:33:15 +02:00
dcccb544f9 fix(MySQL): invalid JavaScript datetime values not shown 2021-04-16 18:48:56 +02:00
7d2ace9456 fix: field apparently loses index or foreign key on rename in table editor 2021-04-16 17:42:16 +02:00
2584c9b9c2 chore: replaced link for donations with Treedom 2021-04-15 14:55:37 +02:00
a6b75ad0dc fix: approximate row count shown for results less than 1000 2021-04-15 10:13:55 +02:00
90fd9db917 perf(MySQL): improved the way to get routine and functions parameters 2021-04-14 18:06:20 +02:00
c0f54b9514 build: update dependencies 2021-04-14 10:42:00 +02:00
cd31413256 feat(PostgreSQL): functions management 2021-04-13 18:05:03 +02:00
b33199ea59 feat(PostgreSQL): procedure language select 2021-04-12 18:46:35 +02:00
dea5ec7513 chore(release): 0.1.2 2021-04-11 12:39:56 +02:00
be816e8588 perf(UI): improved setting modal rendering 2021-04-11 12:38:50 +02:00
1e938adc5d feat: in-app last release changelog 2021-04-11 12:35:16 +02:00
8735a0c5f9 feat(PostgreSQL): edit timezone in cell editor 2021-04-11 10:55:22 +02:00
3dde1c109e feat(PostgreSQL): procedures management 2021-04-10 20:38:46 +02:00
d0b3e1b1b8 feat(PostgreSQL): support of arrays in table settings 2021-04-09 19:31:41 +02:00
c20bff7bcb fix: deletion of rows from query results 2021-04-08 21:49:38 +02:00
9f5ec0276c fix: no foreign key select when cell value is NULL, closes #50 2021-04-08 18:02:16 +02:00
55932fe115 fix: cell edit doesn't properly use primary or unique index to update if both present, closes #51 2021-04-08 17:47:10 +02:00
d374372e20 fix: wrong datetime conversion when updating a row without an unique key 2021-04-07 15:05:11 +02:00
bb5f44681f fix(UI): white readonly inputs with dark theme 2021-04-07 09:20:11 +02:00
49a4e1cb7b fix(PostgreSQL): issue with selected schema different than public 2021-04-06 12:48:40 +02:00
124 changed files with 6250 additions and 1175 deletions

View File

@@ -9,22 +9,25 @@
"plugin:vue/recommended"
],
"parserOptions": {
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"ecmaVersion": 9,
"sourceType": "module"
"sourceType": "module",
"requireConfigFile": false
},
"rules": {
"indent": [
"error",
3,
{ "SwitchCase": 1 }
{
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
"unix"
],
"brace-style": [
"error",
"error",
"stroustrup"
],
"quotes": [
@@ -36,7 +39,7 @@
"always"
],
"curly": [
"error",
"error",
"multi-or-nest"
],
"no-console": "off",
@@ -44,18 +47,25 @@
"vue/no-side-effects-in-computed-properties": "off",
"vue/require-default-prop": "off",
"vue/no-v-html": "off",
"vue/html-indent": ["error", 3, {
"attribute": 1,
"baseIndent": 1,
"closeBracket": 0,
"ignores": []
}],
"vue/max-attributes-per-line": ["error", {
"singleline": 2,
"multiline": {
"max": 1,
"allowFirstLine": false
"vue/html-indent": [
"error",
3,
{
"attribute": 1,
"baseIndent": 1,
"closeBracket": 0,
"ignores": []
}
}]
],
"vue/max-attributes-per-line": [
"error",
{
"singleline": 2,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}
]
}
}

2
.github/FUNDING.yml vendored
View File

@@ -1,7 +1,7 @@
# These are supported funding model platforms
github: [fabio286]
patreon: fabio286
patreon: #fabio286
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel

View File

@@ -12,6 +12,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -23,9 +24,15 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Application (please complete the following information):**
- Version [e.g. 0.14.0]
- Distribution: [e.g. exe, Linux Store, AppImage, dmg]
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 22]
- OS: [e.g. iOS]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,4 +1,4 @@
name: Build/release
name: Build/release [linux]
on: push
@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
os: [ubuntu-latest]
steps:
- name: Check out Git repository
@@ -17,10 +17,10 @@ jobs:
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 10
node-version: 14
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

26
.github/workflows/build-mac.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Build/release [mac]
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v1
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

26
.github/workflows/build-win.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Build/release [windows]
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v1
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

View File

@@ -9,7 +9,8 @@
],
"rules": {
"at-rule-no-unknown": null,
"no-descending-specificity": null
"no-descending-specificity": null,
"declaration-colon-newline-after": "always-multi-line"
},
"syntax": "scss"
}

View File

@@ -1,52 +0,0 @@
language: node_js
node_js: 12
cache:
directories:
- node_modules
- app/node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
env:
global:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
jobs:
include:
- stage: Test
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script:
- npm test
- stage: Deploy Linux & Windows
if: tag IS present
os: linux
services: docker
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script:
- docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always"
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
- stage: Deploy Mac
if: tag IS present
os: osx
before_install:
- npm install
osx_image: xcode10.2
script:
- npm run build -- -p always
# - stage: Deploy ARM Linux
# if: tag IS present
# os: linux
# arch: arm64
# script:
# - npm run build -- --linux AppImage -p always

View File

@@ -2,6 +2,254 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.2.1](https://github.com/Fabio286/antares/compare/v0.2.0...v0.2.1) (2021-07-09)
### Features
* **UI:** contextual menu shortcuts to create new elements on folders ([0252a06](https://github.com/Fabio286/antares/commit/0252a064d99df09b01c020c7d10c76dd43d4ede8))
* context menu option to duplicate connections ([439356a](https://github.com/Fabio286/antares/commit/439356a01993a6a248f5a7d7df305ac5e0e63775))
* **MySQL:** possibility to set a default schema in connection parameters ([c6897af](https://github.com/Fabio286/antares/commit/c6897af22d04ed930289a55124b3e8d080fbae3a))
* **UI:** new connection add panel ([8cd76e7](https://github.com/Fabio286/antares/commit/8cd76e711c9328254d498b4f9afa221311f5a487))
* **UI:** new connection edit panel ([9af71a6](https://github.com/Fabio286/antares/commit/9af71a6e343deda1bf79d8410511cf861af3c304))
* SSH Tunnel functionality ([#81](https://github.com/Fabio286/antares/issues/81)) ([1801bef](https://github.com/Fabio286/antares/commit/1801bef019cee77a99df7e3822145ad952465abb))
### Bug Fixes
* clear corrupted configurations to avoid exceptions ([56fcc26](https://github.com/Fabio286/antares/commit/56fcc2650b93ece398118f39f027dc9520dd8a6a))
* **UI:** connection tab indicator when scrolling ([a0ab63b](https://github.com/Fabio286/antares/commit/a0ab63bdb533aac9b2bdc2ee07a3b1f2b70ea227))
* avoid to trigger schema loading multiple times ([7570b0a](https://github.com/Fabio286/antares/commit/7570b0add8cb9130f15cf8cb807a96dbfd2837d0))
## [0.2.0](https://github.com/Fabio286/antares/compare/v0.1.13...v0.2.0) (2021-07-03)
### Features
* contextual option to duplicate tables ([08d5b1b](https://github.com/Fabio286/antares/commit/08d5b1b3299adc5c53419f50892aee3b58e6fe72))
* **PostgreSQL:** trigger functions support ([75bbd5f](https://github.com/Fabio286/antares/commit/75bbd5f66e38a971482acff9dde636cf89858c8e))
* **PostgreSQL:** triggers creation ([7df0cf8](https://github.com/Fabio286/antares/commit/7df0cf83895ba784e30efa7814433cf329175364))
* **UI:** active tab animation ([978a7c5](https://github.com/Fabio286/antares/commit/978a7c5f5b314f2ba4067c86471755fdcc341b3f))
* **UI:** context option to copy cell or row value ([d868c77](https://github.com/Fabio286/antares/commit/d868c772b9e7c70e173d1f80ad8de51aa40991ef))
* **UI:** resizer border mouse hover animation ([a69bdeb](https://github.com/Fabio286/antares/commit/a69bdeb20db7d67613dace554ae37a0c5e112ec2))
### Bug Fixes
* **PostgreSQL:** error opening setting tab for some stored routines ([d96907c](https://github.com/Fabio286/antares/commit/d96907ca2de344114e3edae52843f6258468934b))
* **UI:** contextual sub-menu alignment when close to the lower edge of the window ([61a42d5](https://github.com/Fabio286/antares/commit/61a42d51f55ec3010c4ee67bad2f699ff14c9f7c))
* fields default not correctly set in table filler ([faa07a0](https://github.com/Fabio286/antares/commit/faa07a077c899af5c71124e0c20869876e57fd9a))
* rows loses internal id after export ([2d278aa](https://github.com/Fabio286/antares/commit/2d278aa14e0e5080725196538b7265ec00a7aebb))
* **UI:** editor blinking on first connection with a dark theme ([0110756](https://github.com/Fabio286/antares/commit/0110756204b79388535a74017b303e1ba41e8fd8))
* unhandled exception in connection test ([a975df3](https://github.com/Fabio286/antares/commit/a975df38dd471ab0ac8645a1021470a4600e6598))
* vulnerability in server error toast messages ([3aef7e9](https://github.com/Fabio286/antares/commit/3aef7e953ea82a9105d470cc62c68aacfc97f9d9))
### Improvements
* improved contextual menu appearance ([f74bca7](https://github.com/Fabio286/antares/commit/f74bca7bb4fc6802143446a44663fbf12319bf29))
* **UI:** increased application border-radius ([a0a025e](https://github.com/Fabio286/antares/commit/a0a025e4502141e95394ca9717b1ec65df2728c1))
### [0.1.13](https://github.com/Fabio286/antares/compare/v0.1.12...v0.1.13) (2021-06-19)
### Features
* **PostgreSQL:** alter trigger support ([5c8ee66](https://github.com/Fabio286/antares/commit/5c8ee66f432585db7bd72103fa814558bf406bcc))
* **UI:** ability to manually insert page number in DATA tabs, closes [#65](https://github.com/Fabio286/antares/issues/65) ([e2ebb04](https://github.com/Fabio286/antares/commit/e2ebb04a9047d25187889644aa625fe675de808b))
* **UI:** option to change query editors font size, closes [#77](https://github.com/Fabio286/antares/issues/77) ([e579f37](https://github.com/Fabio286/antares/commit/e579f374381b329422b646b8f7ab5acf298db981))
### Bug Fixes
* **MySQL:** pool connections not released after MySQL errors, causing endless load animation ([4133fc4](https://github.com/Fabio286/antares/commit/4133fc452fc2e961eb587590e010f4968675db7e))
* **UI:** various fixes in displaying content with small window size ([8a20bef](https://github.com/Fabio286/antares/commit/8a20befd0941cb2e6d5e29552606454bd871b092))
### Improvements
* remove comments from queries before execution ([690a454](https://github.com/Fabio286/antares/commit/690a4541f96eaf831dea07b777a421729173b654))
### [0.1.12](https://github.com/Fabio286/antares/compare/v0.1.11...v0.1.12) (2021-06-09)
### Features
* **PostgreSQL:** trigger rename and delete ([cce5adb](https://github.com/Fabio286/antares/commit/cce5adbac75170186956211b54f4db5be25aba07))
### Bug Fixes
* internal exceptions ([8742fa1](https://github.com/Fabio286/antares/commit/8742fa10f08c92a98325df50be9a7df8e36b0f2c))
* page offset not reset when selected table changes ([5562e73](https://github.com/Fabio286/antares/commit/5562e73e75c659051a49d61041d1eee9d66ff6e2))
* **MySQL:** view's data tab doesn't work with some views, closes [#71](https://github.com/Fabio286/antares/issues/71) ([9ca059d](https://github.com/Fabio286/antares/commit/9ca059d979161c0132f64db372b4ed7ecc844b2d))
* **UI:** unable to browse view's result pages ([8a7cc2a](https://github.com/Fabio286/antares/commit/8a7cc2a14fa289c2f3d9e9eb1a5435ffbbaab006))
### [0.1.11](https://github.com/Fabio286/antares/compare/v0.1.10...v0.1.11) (2021-06-02)
### Bug Fixes
* **UI:** prevent canc key to trigger delete modal when editing a row ([5e9c88a](https://github.com/Fabio286/antares/commit/5e9c88a7fd656dd69b92ba4384ca81f4e8cdd422))
* table row loses internal id after cell update ([6622756](https://github.com/Fabio286/antares/commit/66227569f4c032298e20d44c253d30109ffde0b2))
* **MySQL:** missing schema altering tables in some conditions ([faa799c](https://github.com/Fabio286/antares/commit/faa799c8ea2329e7ca1b54c9414a060bc8a2a840))
* empty offset in cell update queries ([acc1eeb](https://github.com/Fabio286/antares/commit/acc1eeb0948db35f2c9a4d85d70fb654b41b15e5))
### [0.1.10](https://github.com/Fabio286/antares/compare/v0.1.9...v0.1.10) (2021-05-29)
### Features
* key shortcuts to change DATA tab page ([f61d7ee](https://github.com/Fabio286/antares/commit/f61d7eeaf42406eb88b3285ac4606794f56e88d3))
* option to set DATA tab page size ([e71c756](https://github.com/Fabio286/antares/commit/e71c7568c09bf8ad184753d3d0f14046f1e386d0))
* prev/next buttons to browse the results pages of data tab ([79f033e](https://github.com/Fabio286/antares/commit/79f033e5245e0e744c35540d25a30b1eadc4d603))
### Bug Fixes
* **MySQL:** wrong schema in view data tab select, closes [#71](https://github.com/Fabio286/antares/issues/71) ([310cfaa](https://github.com/Fabio286/antares/commit/310cfaa3c2324b1159a46be566b6af2555dc9732))
* wrong detection of field default expressions in some cases ([04bdd08](https://github.com/Fabio286/antares/commit/04bdd085a56240273133586d23196e52341d5f27))
### [0.1.9](https://github.com/Fabio286/antares/compare/v0.1.8...v0.1.9) (2021-05-23)
### Bug Fixes
* **MySQL:** can't access tables having UNIQUE KEY, closes [#69](https://github.com/Fabio286/antares/issues/69) ([f1636f1](https://github.com/Fabio286/antares/commit/f1636f1528b5765423535f84027fdc853c58ce80))
### [0.1.8](https://github.com/Fabio286/antares/compare/v0.1.7...v0.1.8) (2021-05-22)
### Bug Fixes
* internal id not removed before row update ([0f10c9e](https://github.com/Fabio286/antares/commit/0f10c9e824b679cfebd8a31085236da3d38a7953))
* unable to add new ENUM fields ([1e37f2a](https://github.com/Fabio286/antares/commit/1e37f2a96f660a36f2739fea57ca298059e9c31a))
* unable to delete table rows ([0d61371](https://github.com/Fabio286/antares/commit/0d6137195da9e0e9afe68f6d3ebcd50e913a888e))
### Improvements
* improved the way how field default value are handled ([7a766f0](https://github.com/Fabio286/antares/commit/7a766f04e668868e8844aac1d7d445bc2da21558))
### [0.1.7](https://github.com/Fabio286/antares/compare/v0.1.6...v0.1.7) (2021-05-16)
### Bug Fixes
* row loses internal id after cell update ([854472c](https://github.com/Fabio286/antares/commit/854472c7a3d8442b1eede12f978cad5aa684094e))
* **MySQL:** connection loses schema in some conditions ([6b0b8b1](https://github.com/Fabio286/antares/commit/6b0b8b19d7176ef8647d6f401a33315f8732fdf3))
* issue with ENUM and SET fields on table filler modal ([475397c](https://github.com/Fabio286/antares/commit/475397ca34c5b7d8925e2d97d32216d5b80d8211))
* issue with ENUM and SET length when creating a new field ([7a62131](https://github.com/Fabio286/antares/commit/7a62131cc707aa1c98bb12513de76229472b7c38))
* multiple row select on sorted tables not work properly ([c7663be](https://github.com/Fabio286/antares/commit/c7663be338ccb5ff31e241e824593531ad95bd32))
### Improvements
* **core:** increased connection pool size to improve performance ([ae103e5](https://github.com/Fabio286/antares/commit/ae103e5477ad190c541d0f29e277c41de6947063))
* **MySQL:** improved connections pool handling ([434711a](https://github.com/Fabio286/antares/commit/434711a360bd7539d2ec3adec92e7ccc96fa828e))
### [0.1.6](https://github.com/Fabio286/antares/compare/v0.1.5...v0.1.6) (2021-05-08)
### Features
* **MySQL:** ENUM and SET fields support, closes [#61](https://github.com/Fabio286/antares/issues/61) ([bebba64](https://github.com/Fabio286/antares/commit/bebba64d06532990405763284e27cb768dc050f7))
### Bug Fixes
* better detection and handling of field default type ([3baf6fa](https://github.com/Fabio286/antares/commit/3baf6fa1736a405c95fb02d17c11514861ca9e04))
* no quotes around strings in field default custom value ([29e2d92](https://github.com/Fabio286/antares/commit/29e2d92b5bf66ff9e80bb1fee1274967c4418601))
* **UI:** data type figure twice on type select ([9dfe7cc](https://github.com/Fabio286/antares/commit/9dfe7cca2234ce6de512bb4c21205c2217e7f765))
* support to mDNS/zeroconf in snap build, closes [#58](https://github.com/Fabio286/antares/issues/58) ([35ef070](https://github.com/Fabio286/antares/commit/35ef070725f5779923bf3ad428b44accddf22dbe))
### Improvements
* italian translation updated ([5dc0b0b](https://github.com/Fabio286/antares/commit/5dc0b0bea40f9d5a5944af2f1c5dc6ff3e60c396))
### [0.1.5](https://github.com/Fabio286/antares/compare/v0.1.4...v0.1.5) (2021-04-30)
### Bug Fixes
* **MySQL:** multiple queries non properly split in some cases ([5208ec1](https://github.com/Fabio286/antares/commit/5208ec171b44da0e6bfa93f15bfedd03ef2aa868))
* % character not properly escaped, closes [#60](https://github.com/Fabio286/antares/issues/60) ([ecfb732](https://github.com/Fabio286/antares/commit/ecfb732c265a5485e131e75f3d20ff07d9409753))
* semicolon inside strings breaks queries, closes [#59](https://github.com/Fabio286/antares/issues/59) ([1b09909](https://github.com/Fabio286/antares/commit/1b0990912627a3a4a4e8d62b4593f8a7aa3a7fe5))
### Improvements
* **UI:** new application icon ([5822b3d](https://github.com/Fabio286/antares/commit/5822b3df432e0a2b305d0ff37a20dc466c3a3992))
### [0.1.4](https://github.com/Fabio286/antares/compare/v0.1.3...v0.1.4) (2021-04-22)
### Features
* query results export ([1d363f7](https://github.com/Fabio286/antares/commit/1d363f755e025d0fc6fec61cbd47ff87a8f25728))
* **UI:** canc press to delete selected rows ([20cba6e](https://github.com/Fabio286/antares/commit/20cba6ee9bc1daa902b04d6e2ddcb31d04fbf805))
* **UI:** ctrl+s shortcut to save changes ([16e17b3](https://github.com/Fabio286/antares/commit/16e17b39b6c8b561cc018d02afee2276190ce304))
* **UI:** format and clear queries ([9ffd443](https://github.com/Fabio286/antares/commit/9ffd443a66303f88fc4529896f6d1d7917454f7a))
### Bug Fixes
* launch from shortcut of procedures or functions with parameters without name dont works ([f82dbd2](https://github.com/Fabio286/antares/commit/f82dbd24dcef7b4d8d127a604e256b3f79a6c617))
* wrong changelog in some cases ([a41cf1a](https://github.com/Fabio286/antares/commit/a41cf1ab5662f5f5fdedff4a9e1c626c23071377))
* **UI:** data type not listed in selection if not present in global types ([6eb2977](https://github.com/Fabio286/antares/commit/6eb2977568987b9440b62ae7dbd7183338bfcc9b))
### Improvements
* **UI:** improved connection status indicator ([5ceddb8](https://github.com/Fabio286/antares/commit/5ceddb8e00f3bc1984b8e47de270dc39b367903f))
### [0.1.3](https://github.com/Fabio286/antares/compare/v0.1.2...v0.1.3) (2021-04-17)
### Features
* **PostgreSQL:** functions management ([cd31413](https://github.com/Fabio286/antares/commit/cd3141325681ea572c06b8998dd7bd334ceb3236))
* **PostgreSQL:** procedure language select ([b33199e](https://github.com/Fabio286/antares/commit/b33199ea59c60b467601f333857494aa40adf4e8))
### Bug Fixes
* **MySQL:** invalid JavaScript datetime values not shown ([dcccb54](https://github.com/Fabio286/antares/commit/dcccb544f9ec24ad693c9e81fb4bcfbdbb7cc4e1))
* approximate row count shown for results less than 1000 ([a6b75ad](https://github.com/Fabio286/antares/commit/a6b75ad0dc0d884332464c277e8542b2698630b9))
* field apparently loses index or foreign key on rename in table editor ([7d2ace9](https://github.com/Fabio286/antares/commit/7d2ace94562f8da307b15b83c89d919727d800c8))
### Improvements
* **MySQL:** improved the way to get routine and functions parameters ([90fd9db](https://github.com/Fabio286/antares/commit/90fd9db917c40262f2bc2501ab86f5feba3d8db4))
* **UI:** improved table fields suggestion in query editor ([c22187c](https://github.com/Fabio286/antares/commit/c22187c3053aef368a351cc35e2f1d407ecde209))
### [0.1.2](https://github.com/Fabio286/antares/compare/v0.1.1...v0.1.2) (2021-04-11)
### Features
* in-app last release changelog ([1e938ad](https://github.com/Fabio286/antares/commit/1e938adc5d8eb5ad16ab16342375eecd88f68d20))
* **PostgreSQL:** edit timezone in cell editor ([8735a0c](https://github.com/Fabio286/antares/commit/8735a0c5f9e5b6b3bcaadf37ce158aa7beae2c48))
* **PostgreSQL:** procedures management ([3dde1c1](https://github.com/Fabio286/antares/commit/3dde1c109e23342d94362626ef7350dc123ea859))
* **PostgreSQL:** support of arrays in table settings ([d0b3e1b](https://github.com/Fabio286/antares/commit/d0b3e1b1b8be9d2c038d70e16d4478671315de8f))
### Bug Fixes
* cell edit doesn't properly use primary or unique index to update if both present, closes [#51](https://github.com/Fabio286/antares/issues/51) ([55932fe](https://github.com/Fabio286/antares/commit/55932fe11583bd5ff48f82b8408965adba4f5071))
* deletion of rows from query results ([c20bff7](https://github.com/Fabio286/antares/commit/c20bff7bcbe340ac99ebcacaba3359edd61c068a))
* no foreign key select when cell value is NULL, closes [#50](https://github.com/Fabio286/antares/issues/50) ([9f5ec02](https://github.com/Fabio286/antares/commit/9f5ec0276c92904975fdaea34b4c845c92bfe8d4))
* wrong datetime conversion when updating a row without an unique key ([d374372](https://github.com/Fabio286/antares/commit/d374372e208318d7e50b258a8041145bdf7992c5))
* **PostgreSQL:** issue with selected schema different than public ([49a4e1c](https://github.com/Fabio286/antares/commit/49a4e1cb7b24642641265d5830d3fee370cceeb4))
* **UI:** white readonly inputs with dark theme ([bb5f446](https://github.com/Fabio286/antares/commit/bb5f44681f87aacf2cd2f60a6d958c5732289790))
### Improvements
* **UI:** improved setting modal rendering ([be816e8](https://github.com/Fabio286/antares/commit/be816e85888b4f3d26cbb9caac0adbc4dde0ea94))
### [0.1.1](https://github.com/Fabio286/antares/compare/v0.1.0...v0.1.1) (2021-04-03)

View File

@@ -4,33 +4,35 @@
# Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares)
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Ffabio286%2Fantares%2Fbadge&style=flat)](https://actions-badge.atrox.dev/fabio286/antares/goto) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares) [![Twitter Follow](https://img.shields.io/twitter/follow/AntaresSQL?style=social)](https://twitter.com/AntaresSQL) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL (partially).
Many of its current features are enough to have a pleasant user experience with MySQL/MariaDB, and basic functionalites with PostgreSQL, so give it a chance and send me your feedback, I would really appreciate it.
I'm actively working on it, hoping to provide cool features and fixes as soon as possible.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL.
At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible.
🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest).
👁 To stay tuned for new releases watch this repo on **Release only** channel.
👁 To stay tuned for new releases [follow Antares SQL](https://twitter.com/AntaresSQL) on Twitter.
🌟 Don't forget to **leave a star** if you appreciate this project.
## Philosophy
Why am I developing an SQL client when there are a lot of them on the market?
Why are we developing an SQL client when there are a lot of them on the market?
The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem.
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, tabs or submenu.
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenu; productivity comes first.
## Download
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from Microsoft Store](https://raw.githubusercontent.com/Fabio286/antares/gh-pages/src/assets/ms-store.png)](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from AUR](https://raw.githubusercontent.com/Fabio286/antares/3e00c4bae6e036300c752c1a40c5a038fea9c169/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql/) [![Get it from Microsoft Store](https://raw.githubusercontent.com/Fabio286/antares/gh-pages/src/assets/ms-store.png)](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
🚀 **[Other Downloads](https://github.com/Fabio286/antares/releases/latest)**
## How to contribute
- [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1)
## Current main features
@@ -41,11 +43,11 @@ A modern application created with minimalism and semplicity in mind, with featur
- Fake table data filler.
- Run queries on multiple tabs.
- Query suggestions and auto complete.
- SSH tunnel support.
- Dark and light theme.
- Scratchpad.
- Multi language.
- Secure password storage.
- Auto updates.
## Coming soon
@@ -53,32 +55,18 @@ This is a roadmap with major features will come in near future.
- Support for other databases.
- Database tools.
- SSH tunnel support.
- Users management (add/edit/delete).
- UI/UX improvements.
- Query history.
- Query history and bookmarks.
- More context menu shortcuts.
- More keyboard shortcuts.
- Query logs console.
- Import/export and migration.
## Troubleshooting
### **Linux**
With KDE may need necessary installation of the additional `gnome-keyring` package.
Depending on your distribution, you will need to run the following command:
- Debian/Ubuntu: `sudo apt-get install gnome-keyring`
- Red Hat-based: `sudo yum install gnome-keyring`
- Arch Linux: `sudo pacman -S gnome-keyring`
## Currently supported
### Databases
- [x] MySQL/MariaDB
- [x] PostgreSQL (partially, work in progress)
- [x] PostgreSQL
- [ ] SQLite
- [ ] MSSQL
- [ ] OracleDB
@@ -95,15 +83,17 @@ Depending on your distribution, you will need to run the following command:
#### • ARM
- [ ] Windows
- [ ] Linux
- [x] Linux
- [ ] MacOS
## Translations
**Italian Translation** (46%) / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
**Arabic Translation** (45%) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
**Spanish Translation** (46%) / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
**French Translation** (100%) / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
**Italian Translation** / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
**Arabic Translation** / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
**Spanish Translation** / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
**French Translation** / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
**Portugues (Brasil)** / [Daniel Eduardo](https://github.com/daeleduardo) [[#54](https://github.com/Fabio286/antares/pull/54)]
**Deutsch (Deutschland)** / [Christian Ratz](https://github.com/digitalgopnik) [[#74](https://github.com/Fabio286/antares/pull/74)]
## Reviews

BIN
build/appx/LargeTile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
build/appx/SmallTile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 50 KiB

77
docs/aur-badge.svg Normal file
View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
style="isolation:isolate" viewBox="0 0 182 56" width="182px" height="56px">
<defs>
<clipPath id="_clipPath_tR1uglJ1Zei76xP861DY1TsjAiQWS9qF">
<rect width="182" height="56" />
</clipPath>
</defs>
<g clip-path="url(#_clipPath_tR1uglJ1Zei76xP861DY1TsjAiQWS9qF)">
<path
d="M 2.5 0.5 L 180.5 0.5 C 181.604 0.5 182.5 1.396 182.5 2.5 L 182.5 54.5 C 182.5 55.604 181.604 56.5 180.5 56.5 L 2.5 56.5 C 1.396 56.5 0.5 55.604 0.5 54.5 L 0.5 2.5 C 0.5 1.396 1.396 0.5 2.5 0.5 Z"
style="stroke:none;fill:#252525;stroke-miterlimit:10;" />
<path
d="M 2.5 0.5 L 180.5 0.5 C 181.604 0.5 182.5 1.396 182.5 2.5 L 182.5 54.5 C 182.5 55.604 181.604 56.5 180.5 56.5 L 2.5 56.5 C 1.396 56.5 0.5 55.604 0.5 54.5 L 0.5 2.5 C 0.5 1.396 1.396 0.5 2.5 0.5 Z"
style="fill:none;stroke:#CDCDCD;stroke-width:1;stroke-miterlimit:2;" />
<g>
<g>
<path
d=" M 60.898 13.777 C 58.555 13.774 56.61 14.254 55.858 14.516 L 55.083 18.697 C 55.081 18.712 58.937 17.669 60.635 17.73 C 63.447 17.831 63.706 18.805 63.656 20.119 C 63.704 20.196 62.931 18.931 60.498 18.889 C 57.43 18.836 53.098 19.976 53.104 24.608 C 53.022 29.818 56.997 31.351 59.704 31.379 C 62.138 31.335 63.279 30.458 63.904 29.988 C 64.726 29.129 65.665 28.265 66.561 27.229 C 65.714 28.77 64.978 29.835 64.213 30.651 L 64.213 31.339 L 67.913 30.716 L 67.938 20.66 C 67.901 19.237 68.754 13.791 60.898 13.777 Z M 60.367 22.533 C 61.9 22.554 63.659 23.31 63.662 25.129 C 63.669 26.784 61.589 27.674 60.235 27.66 C 58.881 27.646 57.085 26.596 57.077 24.982 C 57.103 23.54 58.771 22.496 60.367 22.533 Z "
fill-rule="evenodd" fill="rgb(255,255,255)" />
<path
d=" M 70.378 14.707 L 70.352 31.36 L 74.662 30.529 L 74.67 21.087 C 74.671 19.681 76.679 18.039 79.198 18.065 C 79.733 17.097 80.738 14.625 80.983 14.062 C 75.354 14.049 75.282 15.68 74.303 16.483 C 74.293 14.952 74.3 14.033 74.3 14.033 L 70.378 14.707 L 70.378 14.707 Z "
fill-rule="evenodd" fill="rgb(255,255,255)" />
<path
d=" M 94.632 16.893 C 94.591 16.873 92.385 14.312 87.949 14.292 C 83.795 14.223 79.135 15.834 79.061 22.8 C 79.097 28.925 83.537 31.318 87.973 31.365 C 92.72 31.414 94.609 28.396 94.722 28.322 C 94.156 27.83 92.034 25.728 92.034 25.728 C 92.034 25.728 90.709 27.615 88.138 27.639 C 85.566 27.664 83.331 25.651 83.299 22.844 C 83.266 20.036 85.354 18.515 88.157 18.392 C 90.584 18.392 91.984 19.959 91.984 19.959 L 94.632 16.893 L 94.632 16.893 Z "
fill-rule="evenodd" fill="rgb(255,255,255)" />
<path
d=" M 100.065 8.879 L 95.996 9.835 L 96.026 31.526 L 100.034 30.802 L 100.08 20.595 C 100.089 19.524 101.628 17.88 104.2 17.933 C 106.658 17.958 107.207 19.571 107.201 19.775 L 107.272 31.592 L 111.224 30.894 L 111.239 18.363 C 111.265 17.157 108.598 14.61 104.311 14.592 C 102.273 14.596 101.145 15.057 100.571 15.397 C 99.588 16.156 98.466 16.883 97.362 17.811 C 98.382 16.501 99.239 15.595 100.075 14.921 L 100.065 8.879 L 100.065 8.879 Z "
fill-rule="evenodd" fill="rgb(255,255,255)" />
</g>
<g>
<path
d=" M 114.673 9.441 L 116.508 8.982 L 116.595 30.85 L 114.73 31.168 L 114.673 9.441 L 114.673 9.441 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
<path
d=" M 119.663 15.968 L 121.271 15.252 L 121.285 30.932 L 119.731 31.253 L 119.663 15.968 L 119.663 15.968 Z M 119.28 10.314 L 120.577 9.255 L 121.655 10.454 L 120.357 11.54 L 119.28 10.314 L 119.28 10.314 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
<path
d=" M 124.296 15.682 L 126.131 15.308 L 126.14 18.586 C 126.14 18.727 127.148 14.924 132.008 15.009 C 136.727 15.035 137.499 18.688 137.473 19.507 L 137.531 31.034 L 135.922 31.384 L 135.913 19.998 C 135.932 19.665 135.178 16.853 131.843 16.843 C 128.509 16.833 126.199 19.265 126.203 20.818 L 126.229 30.848 L 124.365 31.335 L 124.296 15.682 L 124.296 15.682 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
<path
d=" M 153.547 31.117 L 151.711 31.492 L 151.703 28.214 C 151.703 28.073 150.694 31.876 145.835 31.791 C 141.116 31.765 140.344 28.112 140.37 27.293 L 140.311 15.765 L 142.261 15.372 L 142.292 26.758 C 142.292 27.069 142.665 29.947 145.999 29.957 C 149.334 29.967 151.669 27.949 151.686 24.911 L 151.662 15.928 L 153.477 15.464 L 153.547 31.117 L 153.547 31.117 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
<path
d=" M 157.144 15.553 L 155.857 16.56 L 160.792 23.018 L 155.529 30.478 L 156.894 31.492 L 161.841 24.563 L 166.948 31.656 L 168.211 30.649 L 162.738 23.065 L 167.104 16.933 L 165.762 15.797 L 161.785 21.472 L 157.144 15.553 L 157.144 15.553 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
</g>
<path
d=" M 33.112 3.879 C 30.965 9.143 29.67 12.587 27.279 17.695 C 28.745 19.248 30.544 21.058 33.466 23.101 C 30.325 21.809 28.182 20.511 26.581 19.164 C 23.521 25.549 18.728 34.643 9 52.121 C 16.645 47.707 22.572 44.986 28.095 43.948 C 27.858 42.928 27.723 41.824 27.733 40.673 L 27.742 40.428 C 27.863 35.53 30.411 31.763 33.43 32.019 C 36.448 32.274 38.794 36.455 38.673 41.353 C 38.65 42.275 38.546 43.162 38.364 43.984 C 43.828 45.053 49.691 47.767 57.233 52.121 C 55.746 49.383 54.419 46.915 53.151 44.565 C 51.154 43.017 49.072 41.003 44.823 38.822 C 47.743 39.581 49.834 40.456 51.464 41.435 C 38.575 17.439 37.532 14.251 33.112 3.879 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
<g>
<path
d=" M 170.614 30.156 L 170.614 28.802 L 170.109 28.802 L 170.109 28.621 L 171.325 28.621 L 171.325 28.802 L 170.817 28.802 L 170.817 30.156 L 170.614 30.156 Z "
fill="rgb(23,147,209)" />
<path
d=" M 171.536 30.156 L 171.536 28.621 L 171.842 28.621 L 172.205 29.708 C 172.238 29.809 172.263 29.884 172.278 29.935 C 172.295 29.879 172.323 29.797 172.36 29.689 L 172.727 28.621 L 173 28.621 L 173 30.156 L 172.804 30.156 L 172.804 28.871 L 172.358 30.156 L 172.175 30.156 L 171.732 28.849 L 171.732 30.156 L 171.536 30.156 Z "
fill="rgb(23,147,209)" />
</g>
<g>
<path
d=" M 57.471 47.815 L 57.471 46.493 L 56.977 46.493 L 56.977 46.316 L 58.166 46.316 L 58.166 46.493 L 57.67 46.493 L 57.67 47.815 L 57.471 47.815 Z "
fill="rgb(23,147,209)" />
<path
d=" M 58.372 47.815 L 58.372 46.316 L 58.671 46.316 L 59.026 47.377 C 59.059 47.476 59.083 47.55 59.098 47.599 C 59.115 47.545 59.141 47.465 59.177 47.359 L 59.536 46.316 L 59.803 46.316 L 59.803 47.815 L 59.612 47.815 L 59.612 46.56 L 59.176 47.815 L 58.997 47.815 L 58.564 46.539 L 58.564 47.815 L 58.372 47.815"
fill="rgb(23,147,209)" />
</g>
</g>
<g clip-path="url(#_clipPath_NvFIpNfWUS6M4fZAtfyVzggsKR3URDoi)"><text transform="matrix(1,0,0,1,87.023,43.899)"
style="font-family:'Open Sans';font-weight:700;font-size:11px;font-style:normal;fill:#ffffff;stroke:none;">user
repository</text></g>
<defs>
<clipPath id="_clipPath_NvFIpNfWUS6M4fZAtfyVzggsKR3URDoi">
<rect x="0" y="0" width="86" height="14.98" transform="matrix(1,0,0,1,87,32.142)" />
</clipPath>
</defs>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
docs/gh-logo-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -1,20 +1,22 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.1.1",
"version": "0.2.1",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
"scripts": {
"dev": "cross-env NODE_ENV=development electron-webpack dev",
"compile": "electron-webpack",
"build": "cross-env NODE_ENV=production npm run compile && electron-builder",
"build:appx": "npm run build -- --win appx",
"build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder",
"build:appx": "npm run build:local -- --win appx",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"test": "npm run lint",
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix"
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"postinstall": "electron-builder install-app-deps"
},
"author": "Fabio Di Stasio <fabio286@gmail.com>",
"build": {
@@ -26,24 +28,28 @@
"portable"
]
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
"mac": {
"target": {
"target": "default",
"arch": [
"x64"
]
}
},
"linux": {
"target": [
"deb",
"AppImage"
{
"target": "deb",
"arch": "x64"
},
{
"target": "AppImage",
"arch": [
"x64",
"armv7l",
"arm64"
]
}
],
"category": "Development"
},
@@ -59,6 +65,20 @@
"identityName": "62514FabioDiStasio.AntaresSQLClient",
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
"applicationId": "FabioDiStasio.AntaresSQLClient"
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
}
},
"electronWebpack": {
@@ -67,48 +87,49 @@
}
},
"dependencies": {
"@electron/remote": "^1.2.0",
"@mdi/font": "^5.9.55",
"ace-builds": "^1.4.12",
"electron-log": "^4.3.0",
"electron-store": "^7.0.0",
"electron-updater": "^4.3.5",
"faker": "^5.3.1",
"keytar": "^7.3.0",
"electron-log": "^4.3.5",
"electron-store": "^8.0.0",
"electron-updater": "^4.3.9",
"faker": "^5.5.3",
"marked": "^2.1.1",
"moment": "^2.29.1",
"mssql": "^6.2.3",
"mysql2": "^2.2.5",
"node-sql-parser": "^3.1.0",
"pg": "^8.5.1",
"pgsql-ast-parser": "^7.0.2",
"pgsql-ast-parser": "^7.2.1",
"source-map-support": "^0.5.16",
"spectre.css": "^0.5.9",
"sql-formatter": "^4.0.2",
"ssh2-promise": "^0.1.7",
"v-mask": "^2.2.4",
"vue-i18n": "^8.22.4",
"vue-i18n": "^8.24.4",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0"
"vuex": "^3.6.2"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"@babel/eslint-parser": "^7.14.5",
"cross-env": "^7.0.2",
"electron": "^11.3.0",
"electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1",
"electron": "^13.1.2",
"electron-builder": "^22.11.7",
"electron-devtools-installer": "^3.2.0",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.20.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint": "^7.29.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-vue": "^7.6.0",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.1",
"standard-version": "^9.1.0",
"stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.11.1",
"sass": "^1.35.1",
"sass-loader": "^10.2.0",
"standard-version": "^9.3.0",
"stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0",
"stylelint-scss": "^3.19.0",
"vue": "^2.6.12",
"vue-template-compiler": "^2.6.12",
"vue": "^2.6.14",
"vue-template-compiler": "^2.6.14",
"webpack": "^4.46.0"
}
}

View File

@@ -14,6 +14,9 @@ confinement: strict
architectures:
- build-on: amd64
compression: lzo
layout:
/etc/nsswitch.conf:
bind-file: $SNAP/etc/nsswitch.conf
parts:
antares:
@@ -71,11 +74,28 @@ parts:
cleanup:
after: [antares]
plugin: nil
build-snaps: [ gnome-3-28-1804 ]
build-snaps: [gnome-3-28-1804]
override-prime: |
set -eux
cd /snap/gnome-3-28-1804/current
find . -type f,l -exec rm -f $SNAPCRAFT_PRIME/{} \;
set -eux
cd /snap/gnome-3-28-1804/current
find . -type f,l -exec rm -f $SNAPCRAFT_PRIME/{} \;
mdns-lookup:
# Make resolution of ".local" host names (Zero-Conf/mDNS/DNS-SD)
# working: Take the original nsswitch.conf file from the base
# Snap and add "mdns4_minimal [NOTFOUND=return]" to its "hosts:" line
# Also install corresponding mdns4_minimal plug-in
# See: https://forum.snapcraft.io/t/no-mdns-support-in-snaps-should-core-have-a-modified-nsswitch-conf/
plugin: nil
stage-packages:
- libnss-mdns
override-prime: |
set -eux
sed -Ee 's/^\s*hosts:(\s+)files/hosts:\1files mdns4_minimal \[NOTFOUND=return\]/' /snap/core18/current/etc/nsswitch.conf > $SNAPCRAFT_STAGE/etc/nsswitch.conf
snapcraftctl prime
prime:
- lib/$SNAPCRAFT_ARCH_TRIPLET/libnss_mdns4_minimal*
- etc/nsswitch.conf
apps:
antares:
@@ -93,4 +113,4 @@ apps:
- opengl
- pulseaudio
- removable-media
- unity7
- unity7

View File

@@ -7,6 +7,7 @@ module.exports = {
database: false,
collations: false,
engines: false,
connectionSchema: false,
// Tools
processesList: false,
usersManagement: false,
@@ -16,6 +17,7 @@ module.exports = {
tables: false,
views: false,
triggers: false,
triggerFunctions: false,
routines: false,
functions: false,
schedulers: false,
@@ -23,6 +25,7 @@ module.exports = {
tableAdd: false,
viewAdd: false,
triggerAdd: false,
triggerFunctionAdd: false,
routineAdd: false,
functionAdd: false,
schedulerAdd: false,
@@ -31,6 +34,7 @@ module.exports = {
tableSettings: false,
viewSettings: false,
triggerSettings: false,
triggerFunctionSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
@@ -44,9 +48,29 @@ module.exports = {
comment: false,
collation: false,
definer: false,
arrays: false,
onUpdate: false,
tableArray: false,
viewAlgorithm: false,
viewSqlSecurity: false,
viewUpdateOption: false
viewUpdateOption: false,
procedureDeterministic: false,
procedureDataAccess: false,
procedureSql: false,
procedureContext: false,
procedureLanguage: false,
functionDeterministic: false,
functionDataAccess: false,
functionSql: false,
functionContext: false,
functionLanguage: false,
triggerSql: false,
triggerStatementInCreation: false,
triggerMultipleEvents: false,
triggerTableInName: false,
triggerUpdateColumns: false,
triggerOnlyRename: false,
triggerFunctionSql: false,
triggerFunctionlanguages: false,
parametersLength: false,
languages: false
};

View File

@@ -7,6 +7,7 @@ module.exports = {
defaultUser: 'root',
defaultDatabase: null,
// Core
connectionSchema: true,
collations: true,
engines: true,
// Tools
@@ -46,5 +47,14 @@ module.exports = {
onUpdate: true,
viewAlgorithm: true,
viewSqlSecurity: true,
viewUpdateOption: true
viewUpdateOption: true,
procedureDeterministic: true,
procedureDataAccess: true,
procedureSql: 'BEGIN\r\n\r\nEND',
procedureContext: true,
triggerSql: 'BEGIN\r\n\r\nEND',
functionDeterministic: true,
functionDataAccess: true,
functionSql: 'BEGIN\r\n\r\nEND',
parametersLength: true
};

View File

@@ -13,23 +13,40 @@ module.exports = {
// Structure
tables: true,
views: true,
triggers: false,
routines: false,
functions: false,
schedulers: false,
triggers: true,
triggerFunctions: true,
routines: true,
functions: true,
// Settings
tableAdd: true,
viewAdd: true,
triggerAdd: true,
triggerFunctionAdd: true,
routineAdd: true,
functionAdd: true,
databaseEdit: false,
tableSettings: true,
viewSettings: true,
triggerSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
triggerSettings: true,
triggerFunctionSettings: true,
routineSettings: true,
functionSettings: true,
indexes: true,
foreigns: true,
sortableFields: false,
nullable: true,
arrays: true
tableArray: true,
procedureSql: '$procedure$\r\n\r\n$procedure$',
procedureContext: true,
procedureLanguage: true,
functionSql: '$function$\r\n\r\n$function$',
triggerFunctionSql: '$function$\r\nBEGIN\r\n\r\nEND\r\n$function$',
triggerFunctionlanguages: ['plpgsql'],
functionContext: true,
functionLanguage: true,
triggerSql: 'EXECUTE PROCEDURE ',
triggerStatementInCreation: true,
triggerMultipleEvents: true,
triggerTableInName: true,
triggerOnlyRename: false,
languages: ['sql', 'plpgsql', 'c', 'internal']
};

View File

@@ -277,13 +277,6 @@ module.exports = [
{
group: 'other',
types: [
{
name: 'UNKNOWN',
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'ENUM',
length: true,
@@ -299,5 +292,17 @@ module.exports = [
zerofill: false
}
]
},
{
group: 'unknown',
types: [
{
name: 'UNKNOWN',
length: false,
collation: false,
unsigned: false,
zerofill: false
}
]
}
];

View File

@@ -54,6 +54,7 @@ export const TIME = [
'TIME',
'TIME WITH TIME ZONE'
];
export const DATETIME = [
'DATETIME',
'TIMESTAMP',
@@ -61,6 +62,12 @@ export const DATETIME = [
'TIMESTAMP WITH TIME ZONE'
];
// Used to check datetime fields only
export const HAS_TIMEZONE = [
'TIMESTAMP WITH TIME ZONE',
'TIME WITH TIME ZONE'
];
export const BLOB = [
'BLOB',
'TINYBLOB',

View File

@@ -12,7 +12,7 @@ const regex = new RegExp(pattern);
function sqlEscaper (string) {
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\\%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];
return r[m.indexOf(char)] || char;
});
}

View File

@@ -1,10 +1,7 @@
'use strict';
import { app, BrowserWindow, nativeImage } from 'electron';
import { app, BrowserWindow, /* session, */ nativeImage } from 'electron';
import * as path from 'path';
import crypto from 'crypto';
import { format as formatUrl } from 'url';
import keytar from 'keytar';
import Store from 'electron-store';
import ipcHandlers from './ipc-handlers';
@@ -31,6 +28,7 @@ async function createMainWindow () {
icon: nativeImage.createFromDataURL(icon.default),
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
'web-security': false,
enableRemoteModule: true,
spellcheck: false
@@ -39,26 +37,22 @@ async function createMainWindow () {
backgroundColor: '#1d1d1d'
});
if (isDevelopment) {
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
try {
if (isDevelopment) { //
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer');
window.webContents.openDevTools();
// const { default: installExtension, VUEJS3_DEVTOOLS } = require('electron-devtools-installer');
installExtension(VUEJS_DEVTOOLS)
.then(name => {
console.log(name, 'installed');
})
.catch(err => {
console.log(err);
});
// const oldDevToolsID = session.defaultSession.getAllExtensions().find(ext => ext.name === 'Vue.js devtools').id;
// session.defaultSession.removeExtension(oldDevToolsID);
// const toolName = await installExtension(VUEJS3_DEVTOOLS);
// console.log(toolName, 'installed');
}
else
await window.loadURL(new URL(`file:///${path.join(__dirname, 'index.html')}`).href);
}
else {
await window.loadURL(formatUrl({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file',
slashes: true
}));
catch (err) {
console.log(err);
}
window.on('closed', () => {
@@ -78,6 +72,8 @@ async function createMainWindow () {
if (!gotTheLock)
app.quit();
else {
require('@electron/remote/main').initialize();
// Initialize ipcHandlers
ipcHandlers();
@@ -88,26 +84,19 @@ else {
app.quit();
});
app.on('activate', () => {
app.on('activate', async () => {
// on macOS it is common to re-create a window even after all windows have been closed
if (mainWindow === null)
mainWindow = createMainWindow();
if (mainWindow === null) {
mainWindow = await createMainWindow();
if (isDevelopment)
mainWindow.webContents.openDevTools();
}
});
// create main BrowserWindow when electron is ready
app.on('ready', async () => {
try {
let key = await keytar.getPassword('antares', 'user');
if (!key) {
key = crypto.randomBytes(16).toString('hex');
keytar.setPassword('antares', 'user', key);
}
}
catch (err) {
console.log(err);
}
mainWindow = createMainWindow();
mainWindow = await createMainWindow();
if (isDevelopment)
mainWindow.webContents.openDevTools();
});
}

View File

@@ -1,4 +1,3 @@
import keytar from 'keytar';
import { app, ipcMain } from 'electron';
export default () => {
@@ -7,14 +6,7 @@ export default () => {
});
ipcMain.on('get-key', async event => {
let key = false;
try {
key = await keytar.getPassword('antares', 'user');
}
catch (err) {
console.log(err);
}
const key = false;
event.returnValue = key;
});
};

View File

@@ -8,7 +8,8 @@ export default connections => {
host: conn.host,
port: +conn.port,
user: conn.user,
password: conn.password
password: conn.password,
application_name: 'Antares SQL'
};
if (conn.database)
@@ -23,14 +24,23 @@ export default connections => {
};
}
const connection = ClientsFactory.getConnection({
client: conn.client,
params
});
await connection.connect();
if (conn.ssh) {
params.ssh = {
host: conn.sshHost,
username: conn.sshUser,
password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey
};
}
try {
const connection = await ClientsFactory.getConnection({
client: conn.client,
params
});
await connection.connect();
await connection.select('1+1').run();
connection.destroy();
@@ -50,12 +60,16 @@ export default connections => {
host: conn.host,
port: +conn.port,
user: conn.user,
password: conn.password
password: conn.password,
application_name: 'Antares SQL'
};
if (conn.database)
params.database = conn.database;
if (conn.schema)
params.schema = conn.schema;
if (conn.ssl) {
params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null,
@@ -65,11 +79,21 @@ export default connections => {
};
}
if (conn.ssh) {
params.ssh = {
host: conn.sshHost,
username: conn.sshUser,
password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey
};
}
try {
const connection = ClientsFactory.getConnection({
client: conn.client,
params,
poolSize: 1
poolSize: 5
});
await connection.connect();

View File

@@ -31,6 +31,16 @@ export default (connections) => {
}
});
ipcMain.handle('alter-trigger-function', async (event, params) => {
try {
await connections[params.uid].alterTriggerFunction(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('create-function', async (event, params) => {
try {
await connections[params.uid].createFunction(params);
@@ -40,4 +50,14 @@ export default (connections) => {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('create-trigger-function', async (event, params) => {
try {
await connections[params.uid].createTriggerFunction(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -128,7 +128,7 @@ export default connections => {
if (!query) return;
try {
const result = await connections[uid].raw(query, { nest: true, details: true });
const result = await connections[uid].raw(query, { nest: true, details: true, schema });
return { status: 'success', response: result };
}

View File

@@ -16,13 +16,15 @@ export default (connections) => {
}
});
ipcMain.handle('get-table-data', async (event, { uid, schema, table, sortParams }) => {
ipcMain.handle('get-table-data', async (event, { uid, schema, table, limit, page, sortParams }) => {
try {
const offset = (page - 1) * limit;
const query = connections[uid]
.select('*')
.schema(schema)
.from(table)
.limit(1000);
.limit(limit)
.offset(offset);
if (sortParams && sortParams.field && sortParams.dir)
query.orderBy({ [sortParams.field]: sortParams.dir.toUpperCase() });
@@ -59,6 +61,8 @@ export default (connections) => {
});
ipcMain.handle('update-table-cell', async (event, params) => {
delete params.row._id;
try { // TODO: move to client classes
let escapedParam;
let reload = false;
@@ -157,9 +161,13 @@ export default (connections) => {
ipcMain.handle('delete-table-rows', async (event, params) => {
if (params.primary) {
const idString = params.rows.map(row => typeof row[params.primary] === 'string'
? `"${row[params.primary]}"`
: row[params.primary]).join(',');
const idString = params.rows.map(row => {
const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
return typeof row[fieldName] === 'string'
? `"${row[fieldName]}"`
: row[fieldName];
}).join(',');
try {
const result = await connections[params.uid]
@@ -416,6 +424,16 @@ export default (connections) => {
}
});
ipcMain.handle('duplicate-table', async (event, params) => {
try {
await connections[params.uid].duplicateTable(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('truncate-table', async (event, params) => {
try {
await connections[params.uid].truncateTable(params);

View File

@@ -26,6 +26,7 @@ export class AntaresCore {
groupBy: [],
orderBy: [],
limit: [],
offset: [],
join: [],
update: [],
insert: [],
@@ -109,6 +110,11 @@ export class AntaresCore {
return this;
}
offset (...args) {
this._query.offset = args;
return this;
}
/**
* @param {String | Array} args field = value
* @returns
@@ -134,7 +140,7 @@ export class AntaresCore {
* @returns {Promise}
* @memberof AntaresCore
*/
async run (args) {
run (args) {
const rawQuery = this.getSQL();
this._resetQuery();
return this.raw(rawQuery, args);

View File

@@ -12,6 +12,12 @@ export class ClientsFactory {
* @param {String} args.params.host
* @param {Number} args.params.port
* @param {String} args.params.password
* @param {String=} args.params.database
* @param {String=} args.params.schema
* @param {String} args.params.ssh.host
* @param {String} args.params.ssh.username
* @param {String} args.params.ssh.password
* @param {Number} args.params.ssh.port
* @param {Number=} args.poolSize
* @returns Database Connection
* @memberof ClientsFactory

View File

@@ -1,12 +1,15 @@
'use strict';
import mysql from 'mysql2';
import mysql from 'mysql2/promise';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql';
import * as SSH2Promise from 'ssh2-promise';
export class MySQLClient extends AntaresCore {
constructor (args) {
super(args);
this._schema = null;
this.types = {
0: 'DECIMAL',
1: 'TINYINT',
@@ -100,10 +103,44 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async connect () {
delete this._params.application_name;
const dbConfig = {
host: this._params.host,
port: this._params.port,
user: this._params.user,
password: this._params.password,
ssl: null
};
if (this._params.schema?.length) dbConfig.database = this._params.schema;
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) {
this._ssh = new SSH2Promise({ ...this._params.ssh });
this._tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = this._tunnel.localPort;
}
if (!this._poolSize)
this._connection = mysql.createConnection(this._params);
else
this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize });
this._connection = await mysql.createConnection(dbConfig);
else {
this._connection = mysql.createPool({
...dbConfig,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
return field.string();
else
return next();
}
});
}
}
/**
@@ -111,6 +148,7 @@ export class MySQLClient extends AntaresCore {
*/
destroy () {
this._connection.end();
if (this._ssh) this._ssh.close();
}
/**
@@ -120,6 +158,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
use (schema) {
this._schema = schema;
return this.raw(`USE \`${schema}\``);
}
@@ -130,6 +169,12 @@ export class MySQLClient extends AntaresCore {
*/
async getStructure (schemas) {
const { rows: databases } = await this.raw('SHOW DATABASES');
let filteredDatabases = databases;
if (this._params.schema)
filteredDatabases = filteredDatabases.filter(db => db.Database === this._params.schema);
const { rows: functions } = await this.raw('SHOW FUNCTION STATUS');
const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS');
const { rows: schedulers } = await this.raw('SELECT *, EVENT_SCHEMA AS `Db`, EVENT_NAME AS `Name` FROM information_schema.`EVENTS`');
@@ -137,7 +182,7 @@ export class MySQLClient extends AntaresCore {
const tablesArr = [];
const triggersArr = [];
for (const db of databases) {
for (const db of filteredDatabases) {
if (!schemas.has(db.Database)) continue;
let { rows: tables } = await this.raw(`SHOW TABLE STATUS FROM \`${db.Database}\``);
@@ -159,7 +204,7 @@ export class MySQLClient extends AntaresCore {
}
}
return databases.map(db => {
return filteredDatabases.map(db => {
if (schemas.has(db.Database)) {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => {
@@ -290,29 +335,87 @@ export class MySQLClient extends AntaresCore {
.select('*')
.schema('information_schema')
.from('COLUMNS')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` })
.where({ TABLE_SCHEMA: `= '${this._schema || schema}'`, TABLE_NAME: `= '${table}'` })
.orderBy({ ORDINAL_POSITION: 'ASC' })
.run();
const { rows: fields } = await this.raw(`SHOW CREATE TABLE \`${this._schema || schema}\`.\`${table}\``);
const remappedFields = fields.map(row => {
if (!row['Create Table']) return false;
let n = 0;
return row['Create Table']
.split('')
.reduce((acc, curr) => {
if (curr === ')') n--;
if (n !== 0) acc += curr;
if (curr === '(') n++;
return acc;
}, '')
.replaceAll('\n', '')
.split(',')
.map(f => {
try {
const fieldArr = f.trim().split(' ');
const nameAndType = fieldArr.slice(0, 2);
if (nameAndType[0].charAt(0) !== '`') return false;
const details = fieldArr.slice(2).join(' ');
let defaultValue = null;
if (details.includes('DEFAULT')) {
defaultValue = details.match(/(?<=DEFAULT ).*?$/gs)[0].split(' COMMENT')[0];
const defaultValueArr = defaultValue.split('');
if (defaultValueArr[0] === '\'') {
defaultValueArr.shift();
defaultValueArr.pop();
defaultValue = defaultValueArr.join('');
}
}
const typeAndLength = nameAndType[1].replace(')', '').split('(');
return {
name: nameAndType[0].replaceAll('`', ''),
type: typeAndLength[0].toUpperCase(),
length: typeAndLength[1] ? typeAndLength[1] : null,
default: defaultValue
};
}
catch (err) {
return false;
}
})
.filter(Boolean)
.reduce((acc, curr) => {
acc[curr.name] = curr;
return acc;
}, {});
})[0];
return rows.map(field => {
let numLength = field.COLUMN_TYPE.match(/int\(([^)]+)\)/);
numLength = numLength ? +numLength.pop() : null;
const enumValues = /(enum|set)/.test(field.COLUMN_TYPE)
? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1)
: null;
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE.toUpperCase(),
type: remappedFields ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE,
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION,
numLength,
enumValues,
datePrecision: field.DATETIME_PRECISION,
charLength: field.CHARACTER_MAXIMUM_LENGTH,
nullable: field.IS_NULLABLE.includes('YES'),
unsigned: field.COLUMN_TYPE.includes('unsigned'),
zerofill: field.COLUMN_TYPE.includes('zerofill'),
order: field.ORDINAL_POSITION,
default: field.COLUMN_DEFAULT,
default: remappedFields ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT,
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment'),
@@ -471,7 +574,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropView (params) {
const sql = `DROP VIEW \`${params.view}\``;
const sql = `DROP VIEW \`${this._schema}\`.\`${params.view}\``;
return await this.raw(sql);
}
@@ -483,10 +586,10 @@ export class MySQLClient extends AntaresCore {
*/
async alterView (params) {
const { view } = params;
let sql = `ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} SQL SECURITY ${view.security} VIEW \`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
let sql = `ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} SQL SECURITY ${view.security} VIEW \`${this._schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
if (view.name !== view.oldName)
sql += `; RENAME TABLE \`${view.oldName}\` TO \`${view.name}\``;
sql += `; RENAME TABLE \`${this._schema}\`.\`${view.oldName}\` TO \`${this._schema}\`.\`${view.name}\``;
return await this.raw(sql);
}
@@ -498,7 +601,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async createView (view) {
const sql = `CREATE ALGORITHM = ${view.algorithm} ${view.definer ? `DEFINER=${view.definer} ` : ''}SQL SECURITY ${view.security} VIEW \`${view.name}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
const sql = `CREATE ALGORITHM = ${view.algorithm} ${view.definer ? `DEFINER=${view.definer} ` : ''}SQL SECURITY ${view.security} VIEW \`${this._schema}\`.\`${view.name}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
return await this.raw(sql);
}
@@ -518,8 +621,8 @@ export class MySQLClient extends AntaresCore {
sql: row['SQL Original Statement'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: row.Trigger,
table: row['SQL Original Statement'].match(/(?<=ON `).*?(?=`)/gs)[0],
event1: row['SQL Original Statement'].match(/(BEFORE|AFTER)/gs)[0],
event2: row['SQL Original Statement'].match(/(INSERT|UPDATE|DELETE)/gs)[0]
activation: row['SQL Original Statement'].match(/(BEFORE|AFTER)/gs)[0],
event: row['SQL Original Statement'].match(/(INSERT|UPDATE|DELETE)/gs)[0]
};
})[0];
}
@@ -531,7 +634,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropTrigger (params) {
const sql = `DROP TRIGGER \`${params.trigger}\``;
const sql = `DROP TRIGGER \`${this._schema}\`.\`${params.trigger}\``;
return await this.raw(sql);
}
@@ -564,7 +667,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async createTrigger (trigger) {
const sql = `CREATE ${trigger.definer ? `DEFINER=${trigger.definer} ` : ''}TRIGGER \`${trigger.name}\` ${trigger.event1} ${trigger.event2} ON \`${trigger.table}\` FOR EACH ROW ${trigger.sql}`;
const sql = `CREATE ${trigger.definer ? `DEFINER=${trigger.definer} ` : ''}TRIGGER \`${this._schema}\`.\`${trigger.name}\` ${trigger.activation} ${trigger.event} ON \`${trigger.table}\` FOR EACH ROW ${trigger.sql}`;
return await this.raw(sql, { split: false });
}
@@ -578,7 +681,7 @@ export class MySQLClient extends AntaresCore {
const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
return results.rows.map(async row => {
if (!row['Create Procedure']) {
return {
definer: null,
@@ -592,22 +695,23 @@ export class MySQLClient extends AntaresCore {
};
}
const parameters = row['Create Procedure']
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null];
return {
name: param[1] ? param[1].replaceAll('`', '') : '',
type: type[0].replaceAll('\n', ''),
length: +type[1] ? +type[1].replace(/\D/g, '') : '',
context: param[0] ? param[0].replace('\n', '') : ''
};
}).filter(el => el.name);
const sql = `SELECT *
FROM information_schema.parameters
WHERE SPECIFIC_NAME = '${routine}'
AND SPECIFIC_SCHEMA = '${schema}'
ORDER BY ORDINAL_POSITION
`;
const results = await this.raw(sql);
const parameters = results.rows.map(row => {
return {
name: row.PARAMETER_NAME,
type: row.DATA_TYPE.toUpperCase(),
length: row.NUMERIC_PRECISION || row.DATETIME_PRECISION || row.CHARACTER_MAXIMUM_LENGTH || '',
context: row.PARAMETER_MODE
};
});
let dataAccess = 'CONTAINS SQL';
if (row['Create Procedure'].includes('NO SQL'))
@@ -637,7 +741,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropRoutine (params) {
const sql = `DROP PROCEDURE \`${params.routine}\``;
const sql = `DROP PROCEDURE \`${this._schema}\`.\`${params.routine}\``;
return await this.raw(sql);
}
@@ -677,7 +781,7 @@ export class MySQLClient extends AntaresCore {
}, []).join(',')
: '';
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters})
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${this._schema}\`.\`${routine.name}\`(${parameters})
LANGUAGE SQL
${routine.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${routine.dataAccess}
@@ -698,7 +802,7 @@ export class MySQLClient extends AntaresCore {
const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
return results.rows.map(async row => {
if (!row['Create Function']) {
return {
definer: null,
@@ -714,22 +818,23 @@ export class MySQLClient extends AntaresCore {
};
}
const parameters = row['Create Function']
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[1] ? param[1].replace(')', '').split('(') : ['', null];
const sql = `SELECT *
FROM information_schema.parameters
WHERE SPECIFIC_NAME = '${func}'
AND SPECIFIC_SCHEMA = '${schema}'
ORDER BY ORDINAL_POSITION
`;
return {
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0],
length: +type[1] ? +type[1].replace(/\D/g, '') : ''
};
}).filter(el => el.name);
const results = await this.raw(sql);
const parameters = results.rows.filter(row => row.PARAMETER_MODE).map(row => {
return {
name: row.PARAMETER_NAME,
type: row.DATA_TYPE.toUpperCase(),
length: row.NUMERIC_PRECISION || row.DATETIME_PRECISION || row.CHARACTER_MAXIMUM_LENGTH || '',
context: row.PARAMETER_MODE
};
});
let dataAccess = 'CONTAINS SQL';
if (row['Create Function'].includes('NO SQL'))
@@ -763,7 +868,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropFunction (params) {
const sql = `DROP FUNCTION \`${params.func}\``;
const sql = `DROP FUNCTION \`${this._schema}\`.\`${params.func}\``;
return await this.raw(sql);
}
@@ -801,13 +906,15 @@ export class MySQLClient extends AntaresCore {
return acc;
}, []).join(',');
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${func.name}\`(${parameters}) RETURNS ${func.returns}${func.returnsLength ? `(${func.returnsLength})` : ''}
const body = func.returns ? func.sql : 'BEGIN\n RETURN 0;\nEND';
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${this._schema}\`.\`${func.name}\`(${parameters}) RETURNS ${func.returns || 'SMALLINT'}${func.returnsLength ? `(${func.returnsLength})` : ''}
LANGUAGE SQL
${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${func.dataAccess}
SQL SECURITY ${func.security}
COMMENT '${func.comment}'
${func.sql}`;
${body}`;
return await this.raw(sql, { split: false });
}
@@ -853,7 +960,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropEvent (params) {
const sql = `DROP EVENT \`${params.scheduler}\``;
const sql = `DROP EVENT \`${this._schema}\`.\`${params.scheduler}\``;
return await this.raw(sql);
}
@@ -869,13 +976,13 @@ export class MySQLClient extends AntaresCore {
if (scheduler.execution === 'EVERY' && scheduler.every[0].includes('-'))
scheduler.every[0] = `'${scheduler.every[0]}'`;
const sql = `ALTER ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.oldName}\`
const sql = `ALTER ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${this._schema}\`.\`${scheduler.oldName}\`
ON SCHEDULE
${scheduler.execution === 'EVERY'
? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}`
: `AT '${scheduler.at}'`}
ON COMPLETION${!scheduler.preserve ? ' NOT' : ''} PRESERVE
${scheduler.name !== scheduler.oldName ? `RENAME TO \`${scheduler.name}\`` : ''}
${scheduler.name !== scheduler.oldName ? `RENAME TO \`${this._schema}\`.\`${scheduler.name}\`` : ''}
${scheduler.state}
COMMENT '${scheduler.comment}'
DO ${scheduler.sql}`;
@@ -890,7 +997,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async createEvent (scheduler) {
const sql = `CREATE ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.name}\`
const sql = `CREATE ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${this._schema}\`.\`${scheduler.name}\`
ON SCHEDULE
${scheduler.execution === 'EVERY'
? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}`
@@ -1027,7 +1134,7 @@ export class MySQLClient extends AntaresCore {
engine
} = params;
const sql = `CREATE TABLE \`${name}\` (\`${name}_ID\` INT NULL) COMMENT='${comment}', COLLATE='${collation}', ENGINE=${engine}`;
const sql = `CREATE TABLE \`${this._schema}\`.\`${name}\` (\`${name}_ID\` INT NULL) COMMENT='${comment}', COLLATE='${collation}', ENGINE=${engine}`;
return await this.raw(sql);
}
@@ -1049,7 +1156,7 @@ export class MySQLClient extends AntaresCore {
options
} = params;
let sql = `ALTER TABLE \`${table}\` `;
let sql = `ALTER TABLE \`${this._schema || params.options.schema}\`.\`${table}\` `;
const alterColumns = [];
// OPTIONS
@@ -1061,7 +1168,7 @@ export class MySQLClient extends AntaresCore {
// ADD FIELDS
additions.forEach(addition => {
const typeInfo = this._getTypeInfo(addition.type);
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
const length = typeInfo.length ? addition.enumValues || addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN \`${addition.name}\`
${addition.type.toUpperCase()}${length ? `(${length})` : ''}
@@ -1099,7 +1206,7 @@ export class MySQLClient extends AntaresCore {
// CHANGE FIELDS
changes.forEach(change => {
const typeInfo = this._getTypeInfo(change.type);
const length = typeInfo.length ? change.numLength || change.charLength || change.datePrecision : false;
const length = typeInfo.length ? change.enumValues || change.numLength || change.charLength || change.datePrecision : false;
alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\`
${change.type.toUpperCase()}${length ? `(${length})` : ''}
@@ -1161,11 +1268,22 @@ export class MySQLClient extends AntaresCore {
sql += alterColumns.join(', ');
// RENAME
if (options.name) sql += `; RENAME TABLE \`${table}\` TO \`${options.name}\``;
if (options.name) sql += `; RENAME TABLE \`${this._schema}\`.\`${table}\` TO \`${this._schema}\`.\`${options.name}\``;
return await this.raw(sql);
}
/**
* DUPLICATE TABLE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async duplicateTable (params) {
const sql = `CREATE TABLE \`${this._schema}\`.\`${params.table}_copy\` LIKE \`${this._schema}\`.\`${params.table}\``;
return await this.raw(sql);
}
/**
* TRUNCATE TABLE
*
@@ -1173,7 +1291,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async truncateTable (params) {
const sql = `TRUNCATE TABLE \`${params.table}\``;
const sql = `TRUNCATE TABLE \`${this._schema}\`.\`${params.table}\``;
return await this.raw(sql);
}
@@ -1184,7 +1302,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropTable (params) {
const sql = `DROP TABLE \`${params.table}\``;
const sql = `DROP TABLE \`${this._schema}\`.\`${params.table}\``;
return await this.raw(sql);
}
@@ -1239,7 +1357,10 @@ export class MySQLClient extends AntaresCore {
// LIMIT
const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`;
// OFFSET
const offsetRaw = this._query.offset.length ? `OFFSET ${this._query.offset.join(', ')} ` : '';
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${offsetRaw}${insertRaw}`;
}
/**
@@ -1252,18 +1373,29 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async raw (sql, args) {
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
args = {
nest: false,
details: false,
split: true,
...args
};
const nestTables = args.nest ? '.' : false;
const resultsArr = [];
let paramsArr = [];
const queries = args.split ? sql.split(';') : [sql];
const queries = args.split
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm)
.filter(Boolean)
.map(q => q.trim())
: [sql];
const isPool = typeof this._connection.getConnection === 'function';
const connection = isPool ? await this._connection.getConnection() : this._connection;
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
if (args.schema)
await connection.query(`USE \`${args.schema}\``);
for (const query of queries) {
if (!query) continue;
@@ -1272,87 +1404,90 @@ export class MySQLClient extends AntaresCore {
let keysArr = [];
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
this._connection.query({ sql: query, nestTables }, async (err, response, fields) => {
connection.query({ sql: query, nestTables }).then(async ([response, fields]) => {
timeStop = new Date();
const queryResult = response;
if (err)
reject(err);
else {
let remappedFields = fields
? fields.map(field => {
if (!field || Array.isArray(field))
return false;
let remappedFields = fields
? fields.map(field => {
if (!field || Array.isArray(field))
return false;
const type = this._getType(field);
const type = this._getType(field);
return {
name: field.orgName,
alias: field.name,
orgName: field.orgName,
schema: field.schema,
table: field.table,
tableAlias: field.table,
orgTable: field.orgTable,
type: type.name,
length: type.length
};
}).filter(Boolean)
: [];
if (args.details) {
let cachedTable;
if (remappedFields.length) {
paramsArr = remappedFields.map(field => {
if (field.orgTable) cachedTable = field.orgTable;// Needed for some queries on information_schema
return {
name: field.orgName,
alias: field.name,
orgName: field.orgName,
schema: field.schema,
table: field.table,
tableAlias: field.table,
orgTable: field.orgTable,
type: type.name,
length: type.length
table: field.orgTable || cachedTable,
schema: field.schema || 'INFORMATION_SCHEMA'
};
}).filter(Boolean)
: [];
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
if (args.details) {
let cachedTable;
for (const paramObj of paramsArr) {
if (!paramObj.table || !paramObj.schema) continue;
if (remappedFields.length) {
paramsArr = remappedFields.map(field => {
if (field.orgTable) cachedTable = field.orgTable;// Needed for some queries on information_schema
return {
table: field.orgTable || cachedTable,
schema: field.schema || 'INFORMATION_SCHEMA'
};
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
try { // Column details
const response = await this.getTableColumns(paramObj);
remappedFields = remappedFields.map(field => {
const detailedField = response.find(f => f.name === field.name);
if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema)
field = { ...field, ...detailedField };
return field;
});
}
catch (err) {
if (isPool) connection.release();
reject(err);
}
for (const paramObj of paramsArr) {
if (!paramObj.table || !paramObj.schema) continue;
try { // Column details
const response = await this.getTableColumns(paramObj);
remappedFields = remappedFields.map(field => {
const detailedField = response.find(f => f.name === field.name);
if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema)
field = { ...detailedField, ...field };
return field;
});
}
catch (err) {
reject(err);
}
try { // Key usage (foreign keys)
const response = await this.getKeyUsage(paramObj);
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
reject(err);
}
try { // Key usage (foreign keys)
const response = await this.getKeyUsage(paramObj);
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
if (isPool) connection.release();
reject(err);
}
}
}
resolve({
duration: timeStop - timeStart,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false,
fields: remappedFields,
keys: keysArr
});
}
resolve({
duration: timeStop - timeStart,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false,
fields: remappedFields,
keys: keysArr
});
}).catch((err) => {
if (isPool) connection.release();
reject(err);
});
});
resultsArr.push({ rows, report, fields, keys, duration });
}
if (isPool) connection.release();
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
}
}

View File

@@ -3,6 +3,7 @@ import { Pool, Client, types } from 'pg';
import { parse } from 'pgsql-ast-parser';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/postgresql';
import * as SSH2Promise from 'ssh2-promise';
function pgToString (value) {
return value.toString();
@@ -23,54 +24,16 @@ export class PostgreSQLClient extends AntaresCore {
this.types = {};
for (const key in types.builtins)
this.types[types.builtins[key]] = key;
}
_getType (field) {
let name = this.types[field.columnType];
let length = field.columnLength;
if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name))
length = field.decimals;
if (name === 'TIMESTAMP')
length = 0;
if (field.charsetNr === 63) { // if binary
if (name === 'CHAR')
name = 'BINARY';
else if (name === 'VARCHAR')
name = 'VARBINARY';
}
if (name === 'BLOB') {
switch (length) {
case 765:
name = 'TYNITEXT';
break;
case 196605:
name = 'TEXT';
break;
case 50331645:
name = 'MEDIUMTEXT';
break;
case 4294967295:
name = field.charsetNr === 63 ? 'LONGBLOB' : 'LONGTEXT';
break;
case 255:
name = 'TINYBLOB';
break;
case 65535:
name = 'BLOB';
break;
case 16777215:
name = 'MEDIUMBLOB';
break;
default:
name = field.charsetNr === 63 ? 'BLOB' : 'TEXT';
}
}
return { name, length };
this._arrayTypes = {
_int2: 'SMALLINT',
_int4: 'INTEGER',
_int8: 'BIGINT',
_float4: 'REAL',
_float8: 'DOUBLE PRECISION',
_char: '"CHAR"',
_varchar: 'CHARACTER VARYING'
};
}
_getTypeInfo (type) {
@@ -79,17 +42,45 @@ export class PostgreSQLClient extends AntaresCore {
.filter(_type => _type.name === type.toUpperCase())[0];
}
_getArrayType (type) {
if (Object.keys(this._arrayTypes).includes(type))
return this._arrayTypes[type];
return type.replace('_', '');
}
/**
* @memberof PostgreSQLClient
*/
async connect () {
const dbConfig = {
host: this._params.host,
port: this._params.port,
user: this._params.user,
password: this._params.password,
ssl: null
};
if (this._params.database?.length) dbConfig.database = this._params.database;
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) {
this._ssh = new SSH2Promise({ ...this._params.ssh });
this._tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = this._tunnel.localPort;
}
if (!this._poolSize) {
const client = new Client(this._params);
const client = new Client(dbConfig);
await client.connect();
this._connection = client;
}
else {
const pool = new Pool({ ...this._params, max: this._poolSize });
const pool = new Pool({ ...dbConfig, max: this._poolSize });
this._connection = pool;
}
}
@@ -99,6 +90,7 @@ export class PostgreSQLClient extends AntaresCore {
*/
destroy () {
this._connection.end();
if (this._ssh) this._ssh.close();
}
/**
@@ -109,7 +101,8 @@ export class PostgreSQLClient extends AntaresCore {
*/
use (schema) {
this._schema = schema;
return this.raw(`SET search_path TO '${schema}', '$user'`);
if (schema)
return this.raw(`SET search_path TO ${schema}`);
}
/**
@@ -189,41 +182,43 @@ export class PostgreSQLClient extends AntaresCore {
});
// PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.database).map(procedure => {
const remappedProcedures = procedures.filter(procedure => procedure.routine_schema === db.database).map(procedure => {
return {
name: procedure.Name,
type: procedure.Type,
definer: procedure.Definer,
created: procedure.Created,
updated: procedure.Modified,
comment: procedure.Comment,
charset: procedure.character_set_client,
security: procedure.Security_type
name: procedure.routine_name,
type: procedure.routine_type,
security: procedure.security_type
};
});
// FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.database).map(func => {
const remappedFunctions = functions.filter(func => func.routine_schema === db.database && func.data_type !== 'trigger').map(func => {
return {
name: func.routine_name,
type: func.routine_type,
definer: null, // func.Definer,
created: null, // func.Created,
updated: null, // func.Modified,
comment: null, // func.Comment,
charset: null, // func.character_set_client,
security: func.security_type
};
});
// TRIGGER FUNCTIONS
const remappedTriggerFunctions = functions.filter(func => func.routine_schema === db.database && func.data_type === 'trigger').map(func => {
return {
name: func.routine_name,
type: func.routine_type,
security: func.security_type
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.database).map(trigger => {
return {
name: trigger.trigger_name,
name: `${trigger.table_name}.${trigger.trigger_name}`,
orgName: trigger.trigger_name,
timing: trigger.activation,
definer: trigger.definition, // ???
definer: '',
definition: trigger.definition,
event: trigger.event,
table: trigger.table_trigger,
sqlMode: trigger.sql_mode
table: trigger.table_name,
sqlMode: ''
};
});
@@ -233,6 +228,7 @@ export class PostgreSQLClient extends AntaresCore {
functions: remappedFunctions,
procedures: remappedProcedures,
triggers: remappedTriggers,
triggerFunctions: remappedTriggerFunctions,
schedulers: []
};
}
@@ -243,6 +239,7 @@ export class PostgreSQLClient extends AntaresCore {
functions: [],
procedures: [],
triggers: [],
triggerFunctions: [],
schedulers: []
};
}
@@ -256,7 +253,7 @@ export class PostgreSQLClient extends AntaresCore {
* @returns {Object} table scructure
* @memberof PostgreSQLClient
*/
async getTableColumns ({ schema, table }) {
async getTableColumns ({ schema, table }, arrayRemap = true) {
const { rows } = await this
.select('*')
.schema('information_schema')
@@ -266,10 +263,17 @@ export class PostgreSQLClient extends AntaresCore {
.run();
return rows.map(field => {
let type = field.data_type;
const isArray = type === 'ARRAY';
if (isArray && arrayRemap)
type = this._getArrayType(field.udt_name);
return {
name: field.column_name,
key: null,
type: field.data_type.toUpperCase(),
type: type.toUpperCase(),
isArray,
schema: field.table_schema,
table: field.table_name,
numPrecision: field.numeric_precision,
@@ -297,6 +301,9 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async getTableIndexes ({ schema, table }) {
if (schema !== 'public')
await this.use(schema);
const { rows } = await this.raw(`WITH ndx_list AS (
SELECT pg_index.indexrelid, pg_class.oid
FROM pg_index, pg_class
@@ -476,7 +483,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropView (params) {
const sql = `DROP VIEW ${params.view}`;
const sql = `DROP VIEW ${this._schema}.${params.view}`;
return await this.raw(sql);
}
@@ -488,10 +495,10 @@ export class PostgreSQLClient extends AntaresCore {
*/
async alterView (params) {
const { view } = params;
let sql = `CREATE OR REPLACE VIEW ${view.oldName} AS ${view.sql}`;
let sql = `CREATE OR REPLACE VIEW ${this._schema}.${view.oldName} AS ${view.sql}`;
if (view.name !== view.oldName)
sql += `; ALTER VIEW ${view.oldName} RENAME TO ${view.name}`;
sql += `; ALTER VIEW ${this._schema}.${view.oldName} RENAME TO ${view.name}`;
return await this.raw(sql);
}
@@ -503,7 +510,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async createView (view) {
const sql = `CREATE VIEW ${view.name} AS ${view.sql}`;
const sql = `CREATE VIEW ${this._schema}.${view.name} AS ${view.sql}`;
return await this.raw(sql);
}
@@ -514,17 +521,33 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async getTriggerInformations ({ schema, trigger }) {
const sql = `SHOW CREATE TRIGGER \`${schema}\`.\`${trigger}\``;
const results = await this.raw(sql);
const [table, triggerName] = trigger.split('.');
const results = await this.raw(`
SELECT event_object_schema AS table_schema,
event_object_table AS table_name,
trigger_schema,
trigger_name,
string_agg(event_manipulation, ',') AS event,
action_timing AS activation,
action_condition AS condition,
action_statement AS definition
FROM information_schema.triggers
WHERE trigger_schema = '${schema}'
AND trigger_name = '${triggerName}'
AND event_object_table = '${table}'
GROUP BY 1,2,3,4,6,7,8
ORDER BY table_schema,
table_name
`);
return results.rows.map(row => {
return {
definer: row['SQL Original Statement'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['SQL Original Statement'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: row.Trigger,
table: row['SQL Original Statement'].match(/(?<=ON `).*?(?=`)/gs)[0],
event1: row['SQL Original Statement'].match(/(BEFORE|AFTER)/gs)[0],
event2: row['SQL Original Statement'].match(/(INSERT|UPDATE|DELETE)/gs)[0]
sql: row.definition,
name: row.trigger_name,
table: row.table_name,
event: row.event.split(','),
activation: row.activation
};
})[0];
}
@@ -536,7 +559,8 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropTrigger (params) {
const sql = `DROP TRIGGER \`${params.trigger}\``;
const triggerParts = params.trigger.split('.');
const sql = `DROP TRIGGER "${triggerParts[1]}" ON "${triggerParts[0]}"`;
return await this.raw(sql);
}
@@ -553,8 +577,8 @@ export class PostgreSQLClient extends AntaresCore {
try {
await this.createTrigger(tempTrigger);
await this.dropTrigger({ trigger: tempTrigger.name });
await this.dropTrigger({ trigger: trigger.oldName });
await this.dropTrigger({ trigger: `${tempTrigger.table}.${tempTrigger.name}` });
await this.dropTrigger({ trigger: `${trigger.table}.${trigger.oldName}` });
await this.createTrigger(trigger);
}
catch (err) {
@@ -569,7 +593,8 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async createTrigger (trigger) {
const sql = `CREATE ${trigger.definer ? `DEFINER=${trigger.definer} ` : ''}TRIGGER \`${trigger.name}\` ${trigger.event1} ${trigger.event2} ON \`${trigger.table}\` FOR EACH ROW ${trigger.sql}`;
const eventsString = Array.isArray(trigger.event) ? trigger.event.join(' OR ') : trigger.event;
const sql = `CREATE TRIGGER "${trigger.name}" ${trigger.activation} ${eventsString} ON "${trigger.table}" FOR EACH ROW ${trigger.sql}`;
return await this.raw(sql, { split: false });
}
@@ -580,16 +605,16 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async getRoutineInformations ({ schema, routine }) {
const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``;
const sql = `SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${routine}'));`;
const results = await this.raw(sql);
return results.rows.map(row => {
if (!row['Create Procedure']) {
return results.rows.map(async row => {
if (!row.pg_get_functiondef) {
return {
definer: null,
sql: '',
parameters: [],
name: row.Procedure,
name: routine,
comment: '',
security: 'DEFINER',
deterministic: false,
@@ -597,40 +622,48 @@ export class PostgreSQLClient extends AntaresCore {
};
}
const parameters = row['Create Procedure']
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null];
return {
name: param[1] ? param[1].replaceAll('`', '') : '',
type: type[0].replaceAll('\n', ''),
length: +type[1] ? +type[1].replace(/\D/g, '') : '',
context: param[0] ? param[0].replace('\n', '') : ''
};
}).filter(el => el.name);
const sql = `SELECT proc.specific_schema AS procedure_schema,
proc.specific_name,
proc.routine_name AS procedure_name,
proc.external_language,
args.parameter_name,
args.parameter_mode,
args.data_type
FROM information_schema.routines proc
LEFT JOIN information_schema.parameters args
ON proc.specific_schema = args.specific_schema
AND proc.specific_name = args.specific_name
WHERE proc.routine_schema not in ('pg_catalog', 'information_schema')
AND proc.routine_type = 'PROCEDURE'
AND proc.routine_name = '${routine}'
AND proc.specific_schema = '${schema}'
ORDER BY procedure_schema,
specific_name,
procedure_name,
args.ordinal_position
`;
let dataAccess = 'CONTAINS SQL';
if (row['Create Procedure'].includes('NO SQL'))
dataAccess = 'NO SQL';
if (row['Create Procedure'].includes('READS SQL DATA'))
dataAccess = 'READS SQL DATA';
if (row['Create Procedure'].includes('MODIFIES SQL DATA'))
dataAccess = 'MODIFIES SQL DATA';
const results = await this.raw(sql);
const parameters = results.rows.map(row => {
return {
name: row.parameter_name,
type: row.data_type ? row.data_type.toUpperCase() : '',
length: '',
context: row.parameter_mode
};
});
return {
definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
definer: '',
sql: row.pg_get_functiondef.match(/(\$(.*)\$)(.*)(\$(.*)\$)/gs)[0],
parameters: parameters || [],
name: row.Procedure,
comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
deterministic: row['Create Procedure'].includes('DETERMINISTIC'),
dataAccess
name: routine,
comment: '',
security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
deterministic: null,
dataAccess: null,
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0]
};
})[0];
}
@@ -642,7 +675,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropRoutine (params) {
const sql = `DROP PROCEDURE \`${params.routine}\``;
const sql = `DROP PROCEDURE ${this._schema}.${params.routine}`;
return await this.raw(sql);
}
@@ -677,18 +710,18 @@ export class PostgreSQLClient extends AntaresCore {
async createRoutine (routine) {
const parameters = 'parameters' in routine
? routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
acc.push(`${curr.context} ${curr.name} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',')
: '';
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters})
LANGUAGE SQL
${routine.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${routine.dataAccess}
SQL SECURITY ${routine.security}
COMMENT '${routine.comment}'
${routine.sql}`;
if (this._schema !== 'public')
await this.use(this._schema);
const sql = `CREATE PROCEDURE ${this._schema}.${routine.name}(${parameters})
LANGUAGE ${routine.language}
SECURITY ${routine.security}
AS ${routine.sql}`;
return await this.raw(sql, { split: false });
}
@@ -700,63 +733,66 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async getFunctionInformations ({ schema, func }) {
const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``;
const sql = `SELECT pg_get_functiondef((SELECT oid FROM pg_proc WHERE proname = '${func}'));`;
const results = await this.raw(sql);
return results.rows.map(row => {
if (!row['Create Function']) {
return results.rows.map(async row => {
if (!row.pg_get_functiondef) {
return {
definer: null,
sql: '',
parameters: [],
name: row.Procedure,
name: func,
comment: '',
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL',
returns: 'INT',
returnsLength: null
dataAccess: 'CONTAINS SQL'
};
}
const parameters = row['Create Function']
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[1] ? param[1].replace(')', '').split('(') : ['', null];
const sql = `SELECT proc.specific_schema AS procedure_schema,
proc.specific_name,
proc.routine_name AS procedure_name,
proc.external_language,
args.parameter_name,
args.parameter_mode,
args.data_type
FROM information_schema.routines proc
LEFT JOIN information_schema.parameters args
ON proc.specific_schema = args.specific_schema
AND proc.specific_name = args.specific_name
WHERE proc.routine_schema not in ('pg_catalog', 'information_schema')
AND proc.routine_type = 'FUNCTION'
AND proc.routine_name = '${func}'
AND proc.specific_schema = '${schema}'
ORDER BY procedure_schema,
specific_name,
procedure_name,
args.ordinal_position
`;
return {
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0],
length: +type[1] ? +type[1].replace(/\D/g, '') : ''
};
}).filter(el => el.name);
const results = await this.raw(sql);
let dataAccess = 'CONTAINS SQL';
if (row['Create Function'].includes('NO SQL'))
dataAccess = 'NO SQL';
if (row['Create Function'].includes('READS SQL DATA'))
dataAccess = 'READS SQL DATA';
if (row['Create Function'].includes('MODIFIES SQL DATA'))
dataAccess = 'MODIFIES SQL DATA';
const output = row['Create Function'].match(/(?<=RETURNS ).*?(?=\s)/gs).length ? row['Create Function'].match(/(?<=RETURNS ).*?(?=\s)/gs)[0].replace(')', '').split('(') : ['', null];
const parameters = results.rows.filter(row => row.parameter_mode).map(row => {
return {
name: row.parameter_name,
type: row.data_type.toUpperCase(),
length: '',
context: row.parameter_mode
};
});
return {
definer: row['Create Function'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Function'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
definer: '',
sql: row.pg_get_functiondef.match(/(\$(.*)\$)(.*)(\$(.*)\$)/gs)[0],
parameters: parameters || [],
name: row.Function,
comment: row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Function'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
deterministic: row['Create Function'].includes('DETERMINISTIC'),
dataAccess,
returns: output[0].toUpperCase(),
returnsLength: +output[1]
name: func,
comment: '',
security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
deterministic: null,
dataAccess: null,
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0],
returns: row.pg_get_functiondef.match(/(?<=RETURNS )(.*)(?<=[\S+\n\r\s])/gm)[0].replace('SETOF ', '').toUpperCase()
};
})[0];
}
@@ -768,7 +804,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropFunction (params) {
const sql = `DROP FUNCTION \`${params.func}\``;
const sql = `DROP FUNCTION ${this._schema}.${params.func}`;
return await this.raw(sql);
}
@@ -801,112 +837,68 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async createFunction (func) {
const parameters = func.parameters.reduce((acc, curr) => {
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
const parameters = 'parameters' in func
? func.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} ${curr.name} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',')
: '';
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${func.name}\`(${parameters}) RETURNS ${func.returns}${func.returnsLength ? `(${func.returnsLength})` : ''}
LANGUAGE SQL
${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${func.dataAccess}
SQL SECURITY ${func.security}
COMMENT '${func.comment}'
${func.sql}`;
if (this._schema !== 'public')
await this.use(this._schema);
const body = func.returns ? func.sql : '$function$\n$function$';
const sql = `CREATE FUNCTION ${this._schema}.${func.name}(${parameters})
RETURNS ${func.returns || 'void'}
LANGUAGE ${func.language}
SECURITY ${func.security}
AS ${body}`;
return await this.raw(sql, { split: false });
}
/**
* SHOW CREATE EVENT
*
* @returns {Array.<Object>} view informations
* @memberof PostgreSQLClient
*/
// async getEventInformations ({ schema, scheduler }) {
// const sql = `SHOW CREATE EVENT \`${schema}\`.\`${scheduler}\``;
// const results = await this.raw(sql);
// return results.rows.map(row => {
// const schedule = row['Create Event'];
// const execution = schedule.includes('EVERY') ? 'EVERY' : 'ONCE';
// const every = execution === 'EVERY' ? row['Create Event'].match(/(?<=EVERY )(\s*([^\s]+)){0,2}/gs)[0].replaceAll('\'', '').split(' ') : [];
// const starts = execution === 'EVERY' && schedule.includes('STARTS') ? schedule.match(/(?<=STARTS ').*?(?='\s)/gs)[0] : '';
// const ends = execution === 'EVERY' && schedule.includes('ENDS') ? schedule.match(/(?<=ENDS ').*?(?='\s)/gs)[0] : '';
// const at = execution === 'ONCE' && schedule.includes('AT') ? schedule.match(/(?<=AT ').*?(?='\s)/gs)[0] : '';
// return {
// definer: row['Create Event'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
// sql: row['Create Event'].match(/(?<=DO )(.*)/gs)[0],
// name: row.Event,
// comment: row['Create Event'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Event'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
// state: row['Create Event'].includes('ENABLE') ? 'ENABLE' : row['Create Event'].includes('DISABLE ON SLAVE') ? 'DISABLE ON SLAVE' : 'DISABLE',
// preserve: row['Create Event'].includes('ON COMPLETION PRESERVE'),
// execution,
// every,
// starts,
// ends,
// at
// };
// })[0];
// }
/**
* DROP EVENT
* ALTER TRIGGER FUNCTION
*
* @returns {Array.<Object>} parameters
* @memberof PostgreSQLClient
*/
// async dropEvent (params) {
// const sql = `DROP EVENT \`${params.scheduler}\``;
// return await this.raw(sql);
// }
async alterTriggerFunction (params) {
const { func } = params;
if (this._schema !== 'public')
await this.use(this._schema);
const body = func.returns ? func.sql : '$function$\n$function$';
const sql = `CREATE OR REPLACE FUNCTION ${this._schema}.${func.name}()
RETURNS TRIGGER
LANGUAGE ${func.language}
AS ${body}`;
return await this.raw(sql, { split: false });
}
/**
* ALTER EVENT
* CREATE TRIGGER FUNCTION
*
* @returns {Array.<Object>} parameters
* @memberof PostgreSQLClient
*/
// async alterEvent (params) {
// const { scheduler } = params;
async createTriggerFunction (func) {
if (this._schema !== 'public')
await this.use(this._schema);
// if (scheduler.execution === 'EVERY' && scheduler.every[0].includes('-'))
// scheduler.every[0] = `'${scheduler.every[0]}'`;
const body = func.returns ? func.sql : '$function$\r\nBEGIN\r\n\r\nEND\r\n$function$';
// const sql = `ALTER ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.oldName}\`
// ON SCHEDULE
// ${scheduler.execution === 'EVERY'
// ? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}`
// : `AT '${scheduler.at}'`}
// ON COMPLETION${!scheduler.preserve ? ' NOT' : ''} PRESERVE
// ${scheduler.name !== scheduler.oldName ? `RENAME TO \`${scheduler.name}\`` : ''}
// ${scheduler.state}
// COMMENT '${scheduler.comment}'
// DO ${scheduler.sql}`;
const sql = `CREATE FUNCTION ${this._schema}.${func.name}()
RETURNS TRIGGER
LANGUAGE ${func.language}
AS ${body}`;
// return await this.raw(sql, { split: false });
// }
/**
* CREATE EVENT
*
* @returns {Array.<Object>} parameters
* @memberof PostgreSQLClient
*/
// async createEvent (scheduler) {
// const sql = `CREATE ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.name}\`
// ON SCHEDULE
// ${scheduler.execution === 'EVERY'
// ? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}`
// : `AT '${scheduler.at}'`}
// ON COMPLETION${!scheduler.preserve ? ' NOT' : ''} PRESERVE
// ${scheduler.state}
// COMMENT '${scheduler.comment}'
// DO ${scheduler.sql}`;
// return await this.raw(sql, { split: false });
// }
return await this.raw(sql, { split: false });
}
/**
* SELECT * FROM pg_collation
@@ -1000,7 +992,7 @@ export class PostgreSQLClient extends AntaresCore {
name
} = params;
const sql = `CREATE TABLE ${name} (${name}_id INTEGER NULL); ALTER TABLE ${name} DROP COLUMN ${name}_id`;
const sql = `CREATE TABLE ${this._schema}.${name} (${name}_id INTEGER NULL); ALTER TABLE ${this._schema}.${name} DROP COLUMN ${name}_id`;
return await this.raw(sql);
}
@@ -1022,6 +1014,9 @@ export class PostgreSQLClient extends AntaresCore {
options
} = params;
if (this._schema !== 'public')
await this.use(this._schema);
let sql = '';
const alterColumns = [];
const renameColumns = [];
@@ -1040,7 +1035,7 @@ export class PostgreSQLClient extends AntaresCore {
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN ${addition.name}
${addition.type.toUpperCase()}${length ? `(${length})` : ''}
${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''}
${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'}
@@ -1061,7 +1056,7 @@ export class PostgreSQLClient extends AntaresCore {
else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${addition.name} UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX ${addition.name} ON ${table}(${fields})`);
manageIndexes.push(`CREATE INDEX ${addition.name} ON ${this._schema}.${table}(${fields})`);
});
// ADD FOREIGN KEYS
@@ -1089,24 +1084,22 @@ export class PostgreSQLClient extends AntaresCore {
localType = change.type.toLowerCase();
}
alterColumns.push(`ALTER COLUMN "${change.orgName}" TYPE ${localType}${length ? `(${length})` : ''} USING "${change.orgName}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.orgName}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
alterColumns.push(`ALTER COLUMN "${change.orgName}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
if (['SERIAL', 'SMALLSERIAL', 'BIGSERIAL'].includes(change.type)) {
const sequenceName = `${table}_${change.name}_seq`.replace(' ', '_');
createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.orgName}"`);
alterColumns.push(`ALTER COLUMN "${change.orgName}" SET DEFAULT nextval('${sequenceName}')`);
createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.name}"`);
alterColumns.push(`ALTER COLUMN "${change.name}" SET DEFAULT nextval('${sequenceName}')`);
}
if (change.orgName !== change.name)
renameColumns.push(`ALTER TABLE "${table}" RENAME COLUMN "${change.orgName}" TO "${change.name}"`);
renameColumns.push(`ALTER TABLE "${this._schema}"."${table}" RENAME COLUMN "${change.orgName}" TO "${change.name}"`);
});
// CHANGE INDEX
indexChanges.changes.forEach(change => {
if (change.oldType === 'PRIMARY')
alterColumns.push('DROP PRIMARY KEY');
else if (change.oldType === 'UNIQUE')
if (['PRIMARY', 'UNIQUE'].includes(change.oldType))
alterColumns.push(`DROP CONSTRAINT ${change.oldName}`);
else
manageIndexes.push(`DROP INDEX ${change.oldName}`);
@@ -1119,7 +1112,7 @@ export class PostgreSQLClient extends AntaresCore {
else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${change.name} UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX ${change.name} ON ${table}(${fields})`);
manageIndexes.push(`CREATE INDEX ${change.name} ON ${this._schema}.${table}(${fields})`);
});
// CHANGE FOREIGN KEYS
@@ -1146,14 +1139,25 @@ export class PostgreSQLClient extends AntaresCore {
alterColumns.push(`DROP CONSTRAINT ${deletion.constraintName}`);
});
if (alterColumns.length) sql += `ALTER TABLE "${table}" ${alterColumns.join(', ')}; `;
// RENAME
if (renameColumns.length) sql += `${renameColumns.join(';')}; `;
if (alterColumns.length) sql += `ALTER TABLE "${this._schema}"."${table}" ${alterColumns.join(', ')}; `;
if (createSequences.length) sql = `${createSequences.join(';')}; ${sql}`;
if (manageIndexes.length) sql = `${manageIndexes.join(';')}; ${sql}`;
if (options.name) sql += `ALTER TABLE "${table}" RENAME TO "${options.name}"; `;
if (options.name) sql += `ALTER TABLE "${this._schema}"."${table}" RENAME TO "${options.name}"; `;
// RENAME
if (renameColumns.length) sql = `${renameColumns.join(';')}; ${sql}`;
return await this.raw(sql);
}
/**
* DUPLICATE TABLE
*
* @returns {Array.<Object>} parameters
* @memberof PostgreSQLClient
*/
async duplicateTable (params) {
const sql = `CREATE TABLE ${this._schema}.${params.table}_copy (LIKE ${this._schema}.${params.table} INCLUDING ALL)`;
return await this.raw(sql);
}
@@ -1164,7 +1168,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async truncateTable (params) {
const sql = `TRUNCATE TABLE ${params.table}`;
const sql = `TRUNCATE TABLE ${this._schema}.${params.table}`;
return await this.raw(sql);
}
@@ -1175,7 +1179,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropTable (params) {
const sql = `DROP TABLE ${params.table}`;
const sql = `DROP TABLE ${this._schema}.${params.table}`;
return await this.raw(sql);
}
@@ -1230,7 +1234,10 @@ export class PostgreSQLClient extends AntaresCore {
// LIMIT
const limitRaw = selectArray.length && this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`;
// OFFSET
const offsetRaw = selectArray.length && this._query.offset.length ? `OFFSET ${this._query.offset.join(', ')} ` : '';
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${offsetRaw}${insertRaw}`;
}
/**
@@ -1243,15 +1250,25 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async raw (sql, args) {
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');
args = {
nest: false,
details: false,
split: true,
...args
};
if (args.schema && args.schema !== 'public')
await this.use(args.schema);
const resultsArr = [];
let paramsArr = [];
const queries = args.split ? sql.split(';') : [sql];
const queries = args.split
? sql.split(/(?!\B'[^']*);(?![^']*'\B)/gm)
.filter(Boolean)
.map(q => q.trim())
: [sql];
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
@@ -1338,7 +1355,7 @@ export class PostgreSQLClient extends AntaresCore {
if (!paramObj.table || !paramObj.schema) continue;
try { // Column details
const columns = await this.getTableColumns(paramObj);
const columns = await this.getTableColumns(paramObj, false);
const indexes = await this.getTableIndexes(paramObj);
remappedFields = remappedFields.map(field => {

View File

@@ -4,28 +4,29 @@
<div id="window-content">
<TheSettingBar />
<div id="main-content" class="container">
<TheAppWelcome v-if="!connections.length" @new-conn="showNewConnModal" />
<div v-else class="columns col-gapless">
<div class="columns col-gapless">
<Workspace
v-for="connection in connections"
:key="connection.uid"
:connection="connection"
/>
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div>
<TheFooter />
<TheNotificationsBoard />
<TheScratchpad v-if="isScratchpad" />
<ModalSettings v-if="isSettingModal" />
<ModalDiscardChanges v-if="isUnsavedDiscardModal" />
<BaseTextEditor class="d-none" value="" />
</div>
<TheFooter />
<TheNotificationsBoard />
<ModalNewConnection v-if="isNewConnModal" />
<TheScratchpad v-if="isScratchpad" />
<ModalSettings v-if="isSettingModal" />
<ModalDiscardChanges v-if="isUnsavedDiscardModal" />
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { ipcRenderer, remote } from 'electron';
import { ipcRenderer } from 'electron';
import { Menu, getCurrentWindow } from '@electron/remote';
export default {
name: 'App',
@@ -34,21 +35,20 @@ export default {
TheSettingBar: () => import(/* webpackChunkName: "TheSettingBar" */'@/components/TheSettingBar'),
TheFooter: () => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter'),
TheNotificationsBoard: () => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard'),
TheAppWelcome: () => import(/* webpackChunkName: "TheAppWelcome" */'@/components/TheAppWelcome'),
Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'),
ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'),
WorkspaceAddConnectionPanel: () => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel'),
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'),
TheScratchpad: () => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad'),
ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges')
ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges'),
BaseTextEditor: () => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor')
},
data () {
return {};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
isLoading: 'application/isLoading',
isNewConnModal: 'application/isNewModal',
isEditModal: 'application/isEditModal',
isSettingModal: 'application/isSettingModal',
isScratchpad: 'application/isScratchpad',
connections: 'connections/getConnections',
@@ -58,8 +58,7 @@ export default {
},
mounted () {
ipcRenderer.send('check-for-updates');
const Menu = remote.Menu;
this.checkVersionUpdate();
const InputMenu = Menu.buildFromTemplate([
{
@@ -91,7 +90,7 @@ export default {
while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup(remote.getCurrentWindow());
InputMenu.popup(getCurrentWindow());
break;
}
node = node.parentNode;
@@ -100,7 +99,8 @@ export default {
},
methods: {
...mapActions({
showNewConnModal: 'application/showNewConnModal'
showNewConnModal: 'application/showNewConnModal',
checkVersionUpdate: 'application/checkVersionUpdate'
})
}
};

View File

@@ -1,5 +1,5 @@
<template>
<div class="context">
<div class="context" :class="{'bottom': isBottom}">
<a
class="context-overlay"
@click="close"
@@ -23,18 +23,21 @@ export default {
},
data () {
return {
contextSize: null
contextSize: null,
isBottom: false
};
},
computed: {
position () {
const { clientY, clientX } = this.contextEvent;
let topCord = `${clientY + 5}px`;
let topCord = `${clientY + 2}px`;
let leftCord = `${clientX + 5}px`;
if (this.contextSize) {
if (clientY + this.contextSize.height + 5 >= window.innerHeight)
topCord = `${clientY - this.contextSize.height}px`;
if (clientY + (this.contextSize.height < 200 ? 200 : this.contextSize.height) + 5 >= window.innerHeight) {
topCord = `${clientY + 3 - this.contextSize.height}px`;
this.isBottom = true;
}
if (clientX + this.contextSize.width + 5 >= window.innerWidth)
leftCord = `${clientX - this.contextSize.width}px`;
@@ -83,12 +86,21 @@ export default {
left: 0;
bottom: 0;
&:not(.bottom) .context-submenu {
top: -0.2rem;
}
&.bottom .context-submenu {
bottom: -0.2rem;
}
.context-container {
min-width: 100px;
z-index: 10;
padding: 0;
background: #1d1d1d;
border-radius: 0.1rem;
border-radius: $border-radius;
border: 1px solid $bg-color-light-dark;
display: flex;
flex-direction: column;
position: absolute;
@@ -97,19 +109,24 @@ export default {
.context-element {
display: flex;
align-items: center;
margin: 0.2rem;
padding: 0.1rem 0.3rem;
border-radius: $border-radius;
cursor: pointer;
justify-content: space-between;
position: relative;
white-space: nowrap;
.context-submenu {
border-radius: $border-radius;
border: 1px solid $bg-color-light-dark;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s;
position: absolute;
left: 100%;
top: 0;
min-width: 100px;
background: #1d1d1d;
}
&:hover {

View File

@@ -34,6 +34,7 @@ export default {
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
editorFontSize: 'settings/getEditorFontSize',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
})
@@ -47,6 +48,19 @@ export default {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
editorFontSize () {
const sizes = {
small: '12px',
medium: '14px',
large: '16px'
};
if (this.editor) {
this.editor.setOptions({
fontSize: sizes[this.editorFontSize]
});
}
},
autoComplete () {
if (this.editor) {
this.editor.setOptions({
@@ -69,7 +83,7 @@ export default {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value,
value: this.value || '',
fontSize: '14px',
printMargin: false,
readOnly: this.readOnly,

View File

@@ -4,7 +4,7 @@
class="toast mt-2"
:class="toastStatus.className"
>
<span class="p-vcentered text-left" v-html="`${toastStatus.iconTag} ${message}`" />
<span class="p-vcentered text-left"><i class="mdi mdi-24px mr-1" :class="toastStatus.iconName" /> {{ message }}</span>
<button class="btn btn-clear" @click="hideToast" />
</div>
</template>
@@ -30,27 +30,27 @@ export default {
computed: {
toastStatus () {
let className = '';
let iconTag = '';
let iconName = '';
switch (this.status) {
case 'success':
className = 'toast-success';
iconTag = '<i class="mdi mdi-24px mdi-check mr-1"></i>';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconTag = '<i class="mdi mdi-24px mdi-alert-rhombus mr-1"></i>';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconTag = '<i class="mdi mdi-24px mdi-alert mr-1"></i>';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconTag = '<i class="mdi mdi-24px mdi-information-outline mr-1"></i>';
iconName = 'mdi-information-outline';
break;
}
return { className, iconTag };
return { className, iconName };
}
},
watch: {

View File

@@ -63,7 +63,7 @@ export default {
<style lang="scss" scoped>
.file-uploader {
border-radius: 0.1rem;
border-radius: $border-radius;
height: 1.8rem;
line-height: 1.2rem;
display: flex;

View File

@@ -66,6 +66,21 @@
:type="inputProps().type"
:disabled="!isChecked"
>
<select
v-else-if="enumArray"
v-model="selectedValue"
class="form-select"
:disabled="!isChecked"
@change="onChange"
>
<option
v-for="val in enumArray"
:key="val"
:value="val"
>
{{ val }}
</option>
</select>
<input
v-else
ref="formInput"
@@ -121,7 +136,8 @@ export default {
selectedMethod: '',
selectedValue: '',
debounceTimeout: null,
methodParams: {}
methodParams: {},
enumArray: null
};
},
computed: {
@@ -150,8 +166,14 @@ export default {
},
watch: {
fieldObj () {
if (this.fieldObj)
this.selectedValue = this.fieldObj.value;
if (this.fieldObj) {
if (Array.isArray(this.fieldObj.value)) {
this.enumArray = this.fieldObj.value;
this.selectedValue = this.fieldObj.value[0];
}
else
this.selectedValue = this.fieldObj.value;
}
},
selectedGroup () {
if (this.fakerMethods.length)

View File

@@ -51,6 +51,7 @@ export default {
}),
isValidDefault () {
if (!this.foreignList.length) return true;
if (this.value === null) return false;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString());
}
},
@@ -65,7 +66,7 @@ export default {
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type));
const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type) && field.name !== this.keyUsage.refField);
foreignDesc = textField ? textField.name : false;
}
else

View File

@@ -8,25 +8,26 @@
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-play mr-1" /> {{ $t('word.parameters') }}: {{ localRoutine.name }}
<i class="mdi mdi-24px mdi-play mr-1" />
<span class="cut-text">{{ $t('word.parameters') }}: {{ localRoutine.name }}</span>
</div>
</template>
<div slot="body">
<div class="content">
<form class="form-horizontal">
<div
v-for="(parameter, i) in localRoutine.parameters"
v-for="(parameter, i) in inParameters"
:key="parameter._id"
class="form-group"
>
<div class="col-3">
<div class="col-4">
<label class="form-label">{{ parameter.name }}</label>
</div>
<div class="col-9">
<div class="col-8">
<div class="input-group">
<input
:ref="i === 0 ? 'firstInput' : ''"
v-model="values[parameter.name]"
v-model="values[`${i}-${parameter.name}`]"
class="form-input"
type="text"
>
@@ -43,6 +44,7 @@
</template>
<script>
import { NUMBER, FLOAT } from 'common/fieldTypes';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
@@ -57,13 +59,19 @@ export default {
}
},
props: {
localRoutine: Object
localRoutine: Object,
client: String
},
data () {
return {
values: {}
};
},
computed: {
inParameters () {
return this.localRoutine.parameters.filter(param => param.context === 'IN');
}
},
created () {
window.addEventListener('keydown', this.onKey);
@@ -81,8 +89,23 @@ export default {
return '';
},
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr) => {
const value = isNaN(this.values[curr]) ? `"${this.values[curr]}"` : this.values[curr];
const valArr = Object.keys(this.values).reduce((acc, curr, i) => {
let qc;
switch (this.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
}
const param = this.localRoutine.parameters.find(param => `${i}-${param.name}` === curr);
const value = [...NUMBER, ...FLOAT].includes(param.type) ? this.values[curr] : `${qc}${this.values[curr]}${qc}`;
acc.push(value);
return acc;
}, []);

View File

@@ -5,7 +5,8 @@
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editSchema') }}
<i class="mdi mdi-24px mdi-database-edit mr-1" />
<span class="cut-text">{{ $t('message.editSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />

View File

@@ -5,7 +5,8 @@
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> {{ $t('message.tableFiller') }}
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $t('message.tableFiller') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -250,25 +251,32 @@ export default {
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = +field.default;
if ([...TEXT, ...LONG_TEXT].includes(field.type))
fieldDefault = field.default ? field.default.substring(1, field.default.length - 1) : '';
if ([...TIME, ...DATE].includes(field.type))
fieldDefault = Number.isNaN(+field.default) ? null : +field.default;
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = field.default
? field.default.includes('\'')
? field.default.split('\'')[1]
: field.default
: '';
}
else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
if (BIT.includes(field.type))
else if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
if (DATETIME.includes(field.type)) {
if (field.default && field.default.toLowerCase().includes('current_timestamp')) {
else if (DATETIME.includes(field.type)) {
if (field.default && ['current_timestamp', 'now()'].includes(field.default.toLowerCase())) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
else
fieldDefault = field.default;
}
else if (field.enumValues)
fieldDefault = field.enumValues.replaceAll('\'', '').split(',');
else
fieldDefault = field.default;
}
rowObj[field.name] = { value: fieldDefault };

View File

@@ -7,7 +7,7 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewRoutine') }}
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewFunction') }}
</div>
</template>
<div :slot="'body'">
@@ -25,7 +25,19 @@
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
@@ -53,42 +65,7 @@
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.returns') }}
</label>
<div class="column">
<div class="input-group">
<select
v-model="localFunction.returns"
class="form-select text-uppercase"
style="width: 0;"
>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="localFunction.returns === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
<input
v-model="localFunction.returnsLength"
class="form-input"
type="number"
min="0"
>
</div>
</div>
</div>
<div class="form-group">
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
@@ -111,7 +88,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
@@ -124,7 +101,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
@@ -152,11 +129,12 @@ export default {
return {
localFunction: {
definer: '',
sql: 'BEGIN\r\n RETURN NULL;\r\nEND',
sql: '',
parameters: [],
name: '',
comment: '',
returns: 'INT',
language: null,
returns: null,
returnsLength: 10,
security: 'DEFINER',
deterministic: false,
@@ -168,9 +146,17 @@ export default {
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
if (this.customizations.languages)
this.localFunction.language = this.customizations.languages[0];
if (this.customizations.functionSql)
this.localFunction.sql = this.customizations.functionSql;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);

View File

@@ -25,7 +25,19 @@
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localRoutine.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
@@ -53,7 +65,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
@@ -76,7 +88,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
@@ -89,7 +101,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
@@ -117,10 +129,11 @@ export default {
return {
localRoutine: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
sql: '',
parameters: [],
name: '',
comment: '',
language: null,
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
@@ -131,9 +144,17 @@ export default {
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
if (this.customizations.languages)
this.localRoutine.language = this.customizations.languages[0];
if (this.customizations.procedureSql)
this.localRoutine.sql = this.customizations.procedureSql;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);

View File

@@ -5,7 +5,8 @@
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewSchema') }}
<i class="mdi mdi-24px mdi-database-plus mr-1" />
<span class="cut-text">{{ $t('message.createNewSchema') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />

View File

@@ -5,7 +5,8 @@
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> {{ $t('message.addNewRow') }}
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $t('message.addNewRow') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />

View File

@@ -25,7 +25,7 @@
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
@@ -71,30 +71,56 @@
</label>
<div class="column">
<div class="input-group">
<select v-model="localTrigger.event1" class="form-select">
<select v-model="localTrigger.activation" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select v-model="localTrigger.event2" class="form-select">
<option>INSERT</option>
<option>UPDATE</option>
<option>DELETE</option>
<select
v-if="!customizations.triggerMultipleEvents"
v-model="localTrigger.event"
class="form-select"
>
<option v-for="event in Object.keys(localEvents)" :key="event">
{{ event }}
</option>
</select>
<div v-if="customizations.triggerMultipleEvents" class="px-4">
<label
v-for="event in Object.keys(localEvents)"
:key="event"
class="form-checkbox form-inline"
@change.prevent="changeEvents(event)"
>
<input :checked="localEvents[event]" type="checkbox"><i class="form-icon" /> {{ event }}
</label>
</div>
</div>
</div>
</div>
</form>
<div v-if="customizations.triggerStatementInCreation" class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor
ref="queryEditor"
:value.sync="localTrigger.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import QueryEditor from '@/components/QueryEditor';
export default {
name: 'ModalNewTrigger',
components: {
ConfirmModal
ConfirmModal,
QueryEditor
},
props: {
workspace: Object
@@ -103,13 +129,15 @@ export default {
return {
localTrigger: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
sql: '',
name: '',
table: '',
event1: 'BEFORE',
event2: 'INSERT'
activation: 'BEFORE',
event: 'INSERT'
},
isOptionsChanging: false
isOptionsChanging: false,
localEvents: { INSERT: false, UPDATE: false, DELETE: false },
editorHeight: 150
};
},
computed: {
@@ -122,11 +150,16 @@ export default {
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
created () {
this.localTrigger.table = this.schemaTables.length ? this.schemaTables[0].name : '';
this.localTrigger.sql = this.customizations.triggerSql;
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
@@ -134,6 +167,16 @@ export default {
methods: {
confirmNewTrigger () {
this.$emit('open-create-trigger-editor', this.localTrigger);
},
changeEvents (event) {
if (this.customizations.triggerMultipleEvents) {
this.localEvents[event] = !this.localEvents[event];
this.localTrigger.event = [];
for (const key in this.localEvents) {
if (this.localEvents[key])
this.localTrigger.event.push(key);
}
}
}
}
};

View File

@@ -0,0 +1,132 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewFunction"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewFunction') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localFunction.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.triggerFunctionlanguages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localFunction.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewTriggerFunction',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localFunction: {
definer: '',
sql: '',
name: '',
comment: '',
language: null
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
if (this.customizations.triggerFunctionlanguages)
this.localFunction.language = this.customizations.triggerFunctionlanguages[0];
if (this.customizations.triggerFunctionSql)
this.localFunction.sql = this.customizations.triggerFunctionSql;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewFunction () {
this.$emit('open-create-function-editor', this.localFunction);
}
}
};
</script>

View File

@@ -5,7 +5,8 @@
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-memory mr-1" /> {{ $t('message.processesList') }}: {{ connectionName }}
<i class="mdi mdi-24px mdi-memory mr-1" />
<span class="cut-text">{{ $t('message.processesList') }}: {{ connectionName }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />

View File

@@ -6,7 +6,7 @@
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cog mr-1" />
{{ $t('word.settings') }}
<span class="cut-text">{{ $t('word.settings') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
@@ -16,44 +16,51 @@
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item"
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="c-hand">{{ $t('word.general') }}</a>
<a class="tab-link">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item"
class="tab-item c-hand"
:class="{'active': selectedTab === 'themes'}"
@click="selectTab('themes')"
>
<a class="c-hand">{{ $t('word.themes') }}</a>
<a class="tab-link">{{ $t('word.themes') }}</a>
</li>
<li
v-if="updateStatus !== 'disabled'"
class="tab-item"
class="tab-item c-hand"
:class="{'active': selectedTab === 'update'}"
@click="selectTab('update')"
>
<a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
<a class="tab-link" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
</li>
<li
class="tab-item"
class="tab-item c-hand"
:class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')"
>
<a class="tab-link">{{ $t('word.changelog') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'about'}"
@click="selectTab('about')"
>
<a class="c-hand">{{ $t('word.about') }}</a>
<a class="tab-link">{{ $t('word.about') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-4">
<div v-show="selectedTab === 'general'" class="panel-body py-4">
<div class="container">
<form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1">
{{ $t('word.application') }}
</div>
<div class="column col-8 col-sm-12 mb-2">
<div class="form-group mb-4">
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" />
@@ -76,6 +83,27 @@
</select>
</div>
</div>
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
{{ $t('message.dataTabPageSize') }}
</label>
</div>
<div class="col-6 col-sm-12">
<select
v-model="localPageSize"
class="form-select"
@change="changePageSize(+localPageSize)"
>
<option
v-for="size in pageSizes"
:key="size"
>
{{ size }}
</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
@@ -134,7 +162,7 @@
</div>
</div>
<div v-if="selectedTab === 'themes'" class="panel-body py-4">
<div v-show="selectedTab === 'themes'" class="panel-body py-4">
<div class="container">
<div class="columns">
<div class="column col-12 h6 text-uppercase mb-2">
@@ -194,6 +222,31 @@
</optgroup>
</select>
</div>
<div class="column col-6 mb-4">
<div class="btn-group btn-group-block">
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'small'}"
@click="changeEditorFontSize('small')"
>
{{ $t('word.small') }}
</button>
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'medium'}"
@click="changeEditorFontSize('medium')"
>
{{ $t('word.medium') }}
</button>
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'large'}"
@click="changeEditorFontSize('large')"
>
{{ $t('word.large') }}
</button>
</div>
</div>
<div class="column col-12">
<BaseTextEditor
:value="exampleQuery"
@@ -207,17 +260,20 @@
</div>
</div>
<div v-if="selectedTab === 'update'" class="panel-body py-4">
<div v-show="selectedTab === 'update'" class="panel-body py-4">
<ModalSettingsUpdate />
</div>
<div v-show="selectedTab === 'changelog'" class="panel-body py-4">
<ModalSettingsChangelog />
</div>
<div v-if="selectedTab === 'about'" class="panel-body py-4">
<div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center">
<img :src="require('@/images/logo.svg').default" width="128">
<h4>{{ appName }}</h4>
<p>
{{ $t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a> | <a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares/blob/master/CHANGELOG.md')">CHANGELOG</a><br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> Website</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
<small>{{ $t('message.madeWithJS') }}</small>
</p>
@@ -233,6 +289,7 @@
import { mapActions, mapGetters } from 'vuex';
import localesNames from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog';
import BaseTextEditor from '@/components/BaseTextEditor';
const { shell } = require('electron');
@@ -240,14 +297,17 @@ export default {
name: 'ModalSettings',
components: {
ModalSettingsUpdate,
ModalSettingsChangelog,
BaseTextEditor
},
data () {
return {
localLocale: null,
localPageSize: null,
localTimeout: null,
localEditorTheme: null,
selectedTab: 'general',
pageSizes: [40, 100, 250, 500, 1000],
editorThemes: [
{
group: this.$t('word.light'),
@@ -305,11 +365,13 @@ export default {
appVersion: 'application/appVersion',
selectedSettingTab: 'application/selectedSettingTab',
selectedLocale: 'settings/getLocale',
pageSize: 'settings/getDataTabLimit',
selectedAutoComplete: 'settings/getAutoComplete',
selectedLineWrap: 'settings/getLineWrap',
notificationsTimeout: 'settings/getNotificationsTimeout',
applicationTheme: 'settings/getApplicationTheme',
editorTheme: 'settings/getEditorTheme',
editorFontSize: 'settings/getEditorFontSize',
updateStatus: 'application/getUpdateStatus',
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
@@ -347,6 +409,7 @@ ORDER BY
},
created () {
this.localLocale = this.selectedLocale;
this.localPageSize = this.pageSize;
this.localTimeout = this.notificationsTimeout;
this.localEditorTheme = this.editorTheme;
this.selectedTab = this.selectedSettingTab;
@@ -359,10 +422,12 @@ ORDER BY
...mapActions({
closeModal: 'application/hideSettingModal',
changeLocale: 'settings/changeLocale',
changePageSize: 'settings/changePageSize',
changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap',
changeApplicationTheme: 'settings/changeApplicationTheme',
changeEditorTheme: 'settings/changeEditorTheme',
changeEditorFontSize: 'settings/changeEditorFontSize',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}),
selectTab (tab) {
@@ -394,53 +459,59 @@ ORDER BY
<style lang="scss">
#settings {
.modal-body {
overflow: hidden;
.modal-container {
position: absolute;
top: 17.5vh;
.panel-body {
height: calc(70vh - 70px);
overflow: auto;
.modal-body {
overflow: hidden;
.theme-block {
position: relative;
text-align: center;
.panel-body {
min-height: calc(25vh - 70px);
max-height: 65vh;
overflow: auto;
&.selected {
img {
box-shadow: 0 0 0 3px $primary-color;
.theme-block {
position: relative;
text-align: center;
&.selected {
img {
box-shadow: 0 0 0 3px $primary-color;
}
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.theme-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
top: 0;
height: 100%;
width: 100%;
}
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.theme-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
top: 0;
height: 100%;
width: 100%;
}
}
}
.badge::after {
background: #32b643;
}
.badge::after {
background: #32b643;
}
.badge-update::after {
bottom: initial;
background: $primary-color;
}
.badge-update::after {
bottom: initial;
background: $primary-color;
}
.form-label {
display: flex;
align-items: center;
.form-label {
display: flex;
align-items: center;
}
}
}
}

View File

@@ -0,0 +1,82 @@
<template>
<div class="p-relative">
<BaseLoader v-if="isLoading" />
<div
id="changelog"
class="container"
v-html="changelog"
/>
<div v-if="isError" class="empty">
<div class="empty-icon">
<i class="mdi mdi-48px mdi-alert-outline" />
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import marked from 'marked';
import BaseLoader from '@/components/BaseLoader';
export default {
name: 'ModalSettingsChangelog',
components: {
BaseLoader
},
data () {
return {
changelog: '',
isLoading: true,
error: '',
isError: false
};
},
computed: {
...mapGetters({ appVersion: 'application/appVersion' })
},
created () {
this.getChangelog();
},
methods: {
async getChangelog () {
try {
const apiRes = await fetch(`https://api.github.com/repos/Fabio286/antares/releases/tags/v${this.appVersion}`, {
method: 'GET'
});
const { body } = await apiRes.json();
const markdown = body.substr(0, body.indexOf('### Download'));
const renderer = {
link (href, title, text) {
return text;
},
listitem (text) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
}
};
marked.use({ renderer });
this.changelog = marked(markdown);
}
catch (err) {
this.error = err.message;
this.isError = true;
}
this.isLoading = false;
}
}
};
</script>
<style lang="scss">
#changelog {
h3 {
font-size: 1rem;
}
li {
margin-top: 0;
}
}
</style>

View File

@@ -38,6 +38,7 @@ export default {
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
editorFontSize: 'settings/getEditorFontSize',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap',
baseCompleter: 'application/getBaseCompleter'
@@ -126,9 +127,13 @@ export default {
return 'sql';
}
},
cursorPosition () {
return this.editor.session.doc.positionToIndex(this.editor.getCursorPosition());
},
lastWord () {
const words = this.value.split(' ');
return words[words.length - 1];
const charsBefore = this.value.slice(0, this.cursorPosition);
const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean);
return words.pop();
},
isLastWordATable () {
return /\w+\.\w*/gm.test(this.lastWord);
@@ -154,6 +159,19 @@ export default {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
editorFontSize () {
const sizes = {
small: '12px',
medium: '14px',
large: '16px'
};
if (this.editor) {
this.editor.setOptions({
fontSize: sizes[this.editorFontSize]
});
}
},
autoComplete () {
if (this.editor) {
this.editor.setOptions({
@@ -169,8 +187,15 @@ export default {
}
},
isSelected () {
if (this.isSelected)
if (this.isSelected) {
this.lastSchema = this.schema;
this.editor.resize();
}
},
height () {
setTimeout(() => {
this.editor.resize();
}, 20);
},
lastSchema () {
if (this.editor) {
@@ -209,7 +234,7 @@ export default {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
if (this.isLastWordATable || e.args === '.') {
if (e.args !== ' ') {
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop());
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop().trim());
if (table) {
const params = {

View File

@@ -3,18 +3,13 @@
:context-event="contextEvent"
@close-context="$emit('close-context')"
>
<div class="context-element" @click="showEditModal(contextConnection)">
<span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span>
<div class="context-element" @click="duplicateConnection">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ $t('word.duplicate') }}</span>
</div>
<div class="context-element" @click="showConfirmModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}</span>
</div>
<ModalEditConnection
v-if="isEditModal"
:connection="contextConnection"
@close="hideEditModal"
/>
<ConfirmModal
v-if="isConfirmModal"
@confirm="confirmDeleteConnection"
@@ -36,15 +31,14 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditConnection from '@/components/ModalEditConnection';
export default {
name: 'SettingBarContext',
components: {
BaseContextMenu,
ModalEditConnection,
ConfirmModal
},
props: {
@@ -59,7 +53,8 @@ export default {
},
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName'
getConnectionName: 'connections/getConnectionName',
selectedWorkspace: 'workspaces/getSelected'
}),
connectionName () {
return this.getConnectionName(this.contextConnection.uid);
@@ -67,17 +62,25 @@ export default {
},
methods: {
...mapActions({
deleteConnection: 'connections/deleteConnection'
addConnection: 'connections/addConnection',
deleteConnection: 'connections/deleteConnection',
selectWorkspace: 'workspaces/selectWorkspace'
}),
confirmDeleteConnection () {
if (this.selectedWorkspace === this.contextConnection.uid)
this.selectWorkspace();
this.deleteConnection(this.contextConnection);
this.closeContext();
},
showEditModal () {
this.isEditModal = true;
},
hideEditModal () {
this.isEditModal = false;
duplicateConnection () {
let connectionCopy = Object.assign({}, this.contextConnection);
connectionCopy = {
...connectionCopy,
uid: uidGen('C'),
name: connectionCopy.name ? `${connectionCopy.name}_copy` : ''
};
this.addConnection(connectionCopy);
this.closeContext();
},
showConfirmModal () {

View File

@@ -11,9 +11,9 @@
<div class="footer-right-elements">
<ul class="footer-elements">
<li class="footer-element footer-link" @click="openOutside('https://github.com/sponsors/Fabio286')">
<i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small>
<li class="footer-element footer-link" @click="openOutside('https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet')">
<i class="mdi mdi-18px mdi-tree mr-1" />
<small>{{ $t('message.plantATree') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
<i class="mdi mdi-18px mdi-bug" />

View File

@@ -19,13 +19,14 @@
@contextmenu.prevent="contextMenu($event, connection)"
@mouseover.self="tooltipPosition"
>
<i class="settingbar-element-icon dbi" :class="`dbi-${connection.client} ${connected.includes(connection.uid) ? 'badge' : ''}`" />
<i class="settingbar-element-icon dbi" :class="`dbi-${connection.client} ${getStatusBadge(connection.uid)}`" />
<span class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span>
</li>
</draggable>
<li
class="settingbar-element btn btn-link ex-tooltip"
@click="showNewConnModal"
:class="{'selected': 'NEW' === selectedWorkspace}"
@click="selectWorkspace('NEW')"
@mouseover.self="tooltipPosition"
>
<i class="settingbar-element-icon mdi mdi-24px mdi-plus text-light" />
@@ -73,7 +74,7 @@ export default {
...mapGetters({
getConnections: 'connections/getConnections',
getConnectionName: 'connections/getConnectionName',
connected: 'workspaces/getConnected',
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected',
updateStatus: 'application/getUpdateStatus'
}),
@@ -92,7 +93,6 @@ export default {
methods: {
...mapActions({
updateConnections: 'connections/updateConnections',
showNewConnModal: 'application/showNewConnModal',
showSettingModal: 'application/showSettingModal',
showScratchpad: 'application/showScratchpad',
selectWorkspace: 'workspaces/selectWorkspace'
@@ -109,6 +109,22 @@ export default {
const el = e.target;
const fromTop = window.pageYOffset + el.getBoundingClientRect().top - (el.offsetHeight / 4);
el.querySelector('.ex-tooltip-content').style.top = `${fromTop}px`;
},
getStatusBadge (uid) {
if (this.getWorkspace(uid)) {
const status = this.getWorkspace(uid).connection_status;
switch (status) {
case 'connected':
return 'badge badge-connected';
case 'connecting':
return 'badge badge-connecting';
case 'failed':
return 'badge badge-failed';
default:
return '';
}
}
}
}
};
@@ -151,13 +167,13 @@ export default {
height: $settingbar-width;
width: 100%;
margin: 0;
border-left: 3px solid transparent;
opacity: 0.5;
transition: opacity 0.2s;
display: flex;
align-content: center;
justify-content: center;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border-radius: 0;
padding: 0;
&:hover {
opacity: 1;
@@ -165,9 +181,24 @@ export default {
&.selected {
opacity: 1;
&::before {
height: $settingbar-width;
}
}
&::before {
content: '';
height: 0;
width: 3px;
transition: height 0.2s;
background-color: $primary-color;
border-radius: $border-radius;
}
.settingbar-element-icon {
margin: 0 auto;
&.badge::after {
bottom: -10px;
right: 0;
@@ -195,7 +226,7 @@ export default {
padding: 0.2rem 0.4rem;
font-size: 0.7rem;
background: rgba(48, 55, 66, 0.95);
border-radius: 0.1rem;
border-radius: $border-radius;
color: #fff;
max-width: 320px;
pointer-events: none;

View File

@@ -37,15 +37,16 @@
</template>
<script>
import { remote, ipcRenderer } from 'electron';
import { ipcRenderer } from 'electron';
import { getCurrentWindow } from '@electron/remote';
import { mapGetters } from 'vuex';
export default {
name: 'TheTitleBar',
data () {
return {
w: remote.getCurrentWindow(),
isMaximized: remote.getCurrentWindow().isMaximized(),
w: getCurrentWindow(),
isMaximized: getCurrentWindow().isMaximized(),
isDevelopment: process.env.NODE_ENV === 'development'
};
},
@@ -57,6 +58,7 @@ export default {
}),
windowTitle () {
if (!this.selectedWorkspace) return '';
if (this.selectedWorkspace === 'NEW') return this.$t('message.createNewConnection');
const connectionName = this.getConnectionName(this.selectedWorkspace);
const workspace = this.getWorkspace(this.selectedWorkspace);
@@ -132,7 +134,7 @@ export default {
.titlebar-logo {
height: $titlebar-height;
padding: 0 0.4rem;
padding: 0.3rem 0.4rem;
}
.titlebar-element {

View File

@@ -1,7 +1,11 @@
<template>
<div v-show="isSelected" class="workspace column columns col-gapless">
<WorkspaceExploreBar :connection="connection" :is-selected="isSelected" />
<div v-if="workspace.connected" class="workspace-tabs column columns col-gapless">
<WorkspaceExploreBar
v-if="workspace.connection_status === 'connected'"
:connection="connection"
:is-selected="isSelected"
/>
<div v-if="workspace.connection_status === 'connected'" class="workspace-tabs column columns col-gapless">
<ul
id="tabWrap"
ref="tabWrap"
@@ -127,6 +131,12 @@
:connection="connection"
:function="workspace.breadcrumbs.function"
/>
<WorkspacePropsTabTriggerFunction
v-show="selectedTab === 'prop' && workspace.breadcrumbs.triggerFunction"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:function="workspace.breadcrumbs.triggerFunction"
/>
<WorkspacePropsTabScheduler
v-show="selectedTab === 'prop' && workspace.breadcrumbs.scheduler"
:is-selected="selectedTab === 'prop'"
@@ -146,6 +156,7 @@
:connection="connection"
/>
</div>
<WorkspaceEditConnectionPanel v-else :connection="connection" />
<ModalProcessesList
v-if="isProcessesModal"
:connection="connection"
@@ -158,6 +169,7 @@
import { mapGetters, mapActions } from 'vuex';
import Connection from '@/ipc-api/Connection';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel';
import WorkspaceQueryTab from '@/components/WorkspaceQueryTab';
import WorkspaceTableTab from '@/components/WorkspaceTableTab';
import WorkspacePropsTab from '@/components/WorkspacePropsTab';
@@ -165,6 +177,7 @@ import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabTriggerFunction from '@/components/WorkspacePropsTabTriggerFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
import ModalProcessesList from '@/components/ModalProcessesList';
@@ -172,6 +185,7 @@ export default {
name: 'Workspace',
components: {
WorkspaceExploreBar,
WorkspaceEditConnectionPanel,
WorkspaceQueryTab,
WorkspaceTableTab,
WorkspacePropsTab,
@@ -179,6 +193,7 @@ export default {
WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabTriggerFunction,
WorkspacePropsTabScheduler,
ModalProcessesList
},
@@ -208,6 +223,7 @@ export default {
if (this.workspace.breadcrumbs.trigger && this.workspace.customizations.triggerSettings) return true;
if (this.workspace.breadcrumbs.procedure && this.workspace.customizations.routineSettings) return true;
if (this.workspace.breadcrumbs.function && this.workspace.customizations.functionSettings) return true;
if (this.workspace.breadcrumbs.triggerFunction && this.workspace.customizations.functionSettings) return true;
if (this.workspace.breadcrumbs.scheduler && this.workspace.customizations.schedulerSettings) return true;
return false;
},
@@ -219,6 +235,7 @@ export default {
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.triggerFunction === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
) ||
@@ -362,7 +379,7 @@ export default {
min-width: 100%;
.menu-item a {
border-radius: 0.1rem;
border-radius: $border-radius;
color: inherit;
display: block;
margin: 0 -0.4rem;

View File

@@ -0,0 +1,519 @@
<template>
<div class="connection-panel">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="tab-link">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="tab-link">{{ $t('word.ssl') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="tab-link">{{ $t('word.sshTunnel') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
</div>
<div class="column col-8 col-sm-12">
<select v-model="connection.client" class="form-select">
<option value="mysql">
MySQL
</option>
<option value="maria">
MariaDB
</option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div v-if="customizations.database" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="customizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.schema') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.schema"
class="form-input"
type="text"
:placeholder="$t('word.all')"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsl') }}
</label>
</div>
<div class="column col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="connection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.key"
:message="$t('word.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.cert"
:message="$t('word.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.ca"
:message="$t('word.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsh') }}
</label>
</div>
<div class="column col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="connection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="connection.sshKey"
:message="$t('word.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="panel-footer">
<button
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@click="startTest"
>
<i class="mdi mdi-24px mdi-lightning-bolt mr-1" />
{{ $t('message.testConnection') }}
</button>
<button
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy"
@click="saveConnection"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
{{ $t('word.save') }}
</button>
</div>
</div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen';
import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseUploadInput from '@/components/BaseUploadInput';
export default {
name: 'WorkspaceAddConnectionPanel',
components: {
ModalAskCredentials,
BaseUploadInput
},
data () {
return {
connection: {
name: '',
client: 'mysql',
host: '127.0.0.1',
database: null,
port: null,
user: null,
password: '',
ask: false,
uid: uidGen('C'),
ssl: false,
cert: '',
key: '',
ca: '',
ciphers: '',
ssh: false,
sshHost: '',
sshUser: '',
sshPass: '',
sshKey: '',
sshPort: 22
},
isConnecting: false,
isTesting: false,
isAsking: false,
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.connection.client];
},
isBusy () {
return this.isConnecting || this.isTesting;
}
},
created () {
this.setDefaults();
setTimeout(() => {
if (this.$refs.firstInput) this.$refs.firstInput.focus();
}, 20);
},
methods: {
...mapActions({
addConnection: 'connections/addConnection',
connectWorkspace: 'workspaces/connectWorkspace',
addNotification: 'notifications/addNotification',
selectWorkspace: 'workspaces/selectWorkspace'
}),
setDefaults () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
},
async startConnection () {
await this.saveConnection();
this.isConnecting = true;
if (this.connection.ask)
this.isAsking = true;
else {
await this.connectWorkspace(this.connection);
this.isConnecting = false;
}
},
async startTest () {
this.isTesting = true;
if (this.connection.ask)
this.isAsking = true;
else {
try {
const res = await Connection.makeTest(this.connection);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false;
}
},
async continueTest (credentials) { // if "Ask for credentials" is true
this.isAsking = false;
const params = Object.assign({}, this.connection, credentials);
try {
if (this.isConnecting) {
const params = Object.assign({}, this.connection, credentials);
await this.connectWorkspace(params);
this.isConnecting = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false;
},
saveConnection () {
this.selectWorkspace(this.connection.uid);
return this.addConnection(this.connection);
},
closeAsking () {
this.isTesting = false;
this.isAsking = false;
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.connection.ssl = !this.connection.ssl;
},
toggleSsh () {
this.connection.ssh = !this.connection.ssh;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.connection[name] = files[0].path;
},
pathClear (name) {
this.connection[name] = '';
}
}
};
</script>
<style lang="scss" scoped>
.connection-panel {
margin-top: 15vh;
margin-left: auto;
margin-right: auto;
.panel {
width: 450px;
border-radius: $border-radius;
.panel-body {
flex: initial;
}
.panel-footer {
display: flex;
justify-content: flex-end;
}
}
}
</style>

View File

@@ -0,0 +1,500 @@
<template>
<div class="connection-panel">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="tab-link">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="tab-link">{{ $t('word.ssl') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="tab-link">{{ $t('word.sshTunnel') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
ref="firstInput"
v-model="localConnection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
</div>
<div class="column col-8 col-sm-12">
<select v-model="localConnection.client" class="form-select">
<option value="mysql">
MySQL
</option>
<option value="maria">
MariaDB
</option>
<option value="pg">
PostgreSQL
</option>
</select>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.host"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div v-if="customizations.database" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.user"
class="form-input"
type="text"
:disabled="localConnection.ask"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.password"
class="form-input"
type="password"
:disabled="localConnection.ask"
>
</div>
</div>
<div v-if="customizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.schema') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.schema"
class="form-input"
type="text"
:placeholder="$t('word.all')"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsl') }}
</label>
</div>
<div class="column col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="localConnection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssl">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.key"
:message="$t('word.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.cert"
:message="$t('word.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.ca"
:message="$t('word.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
ref="firstInput"
v-model="localConnection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsh') }}
</label>
</div>
<div class="column col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="localConnection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssh">
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="column col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.sshKey"
:message="$t('word.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="panel-footer">
<button
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@click="startTest"
>
<i class="mdi mdi-24px mdi-lightning-bolt mr-1" />
{{ $t('message.testConnection') }}
</button>
<button
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy || !hasChanges"
@click="saveConnection"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
{{ $t('word.save') }}
</button>
<button
class="btn btn-success d-flex"
:class="{'loading': isConnecting}"
:disabled="isBusy"
@click="startConnection"
>
<i class="mdi mdi-24px mdi-connection mr-1" />
{{ $t('word.connect') }}
</button>
</div>
</div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseUploadInput from '@/components/BaseUploadInput';
export default {
name: 'WorkspaceEditConnectionPanel',
components: {
ModalAskCredentials,
BaseUploadInput
},
props: {
connection: Object
},
data () {
return {
isConnecting: false,
isTesting: false,
isAsking: false,
localConnection: null,
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.connection.client];
},
isBusy () {
return this.isConnecting || this.isTesting;
},
hasChanges () {
return JSON.stringify(this.connection) !== JSON.stringify(this.localConnection);
}
},
watch: {
connection () {
this.localConnection = JSON.parse(JSON.stringify(this.connection));
}
},
created () {
this.localConnection = JSON.parse(JSON.stringify(this.connection));
},
methods: {
...mapActions({
editConnection: 'connections/editConnection',
connectWorkspace: 'workspaces/connectWorkspace',
addNotification: 'notifications/addNotification'
}),
async startConnection () {
await this.saveConnection();
this.isConnecting = true;
if (this.localConnection.ask)
this.isAsking = true;
else {
await this.connectWorkspace(this.localConnection);
this.isConnecting = false;
}
},
async startTest () {
this.isTesting = true;
if (this.localConnection.ask)
this.isAsking = true;
else {
try {
const res = await Connection.makeTest(this.localConnection);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false;
}
},
async continueTest (credentials) { // if "Ask for credentials" is true
this.isAsking = false;
const params = Object.assign({}, this.localConnection, credentials);
try {
if (this.isConnecting) {
const params = Object.assign({}, this.connection, credentials);
await this.connectWorkspace(params);
this.isConnecting = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false;
},
saveConnection () {
return this.editConnection(this.localConnection);
},
closeAsking () {
this.isTesting = false;
this.isAsking = false;
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.localConnection.ssl = !this.localConnection.ssl;
},
toggleSsh () {
this.localConnection.ssh = !this.localConnection.ssh;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.localConnection[name] = files[0].path;
},
pathClear (name) {
this.localConnection[name] = '';
}
}
};
</script>
<style lang="scss" scoped>
.connection-panel {
margin-top: 15vh;
margin-left: auto;
margin-right: auto;
.panel {
width: 450px;
border-radius: $border-radius;
.panel-body {
flex: initial;
}
.panel-footer {
display: flex;
justify-content: flex-end;
}
}
}
</style>

View File

@@ -8,7 +8,7 @@
>
<div class="workspace-explorebar-header">
<span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connected" class="workspace-explorebar-tools">
<span v-if="workspace.connection_status === 'connected'" class="workspace-explorebar-tools">
<i
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewSchema')"
@@ -28,7 +28,7 @@
</span>
</div>
<div class="workspace-explorebar-search">
<div v-if="workspace.connected" class="has-icon-right">
<div v-if="workspace.connection_status === 'connected'" class="has-icon-right">
<input
v-model="searchTerm"
class="form-input input-sm"
@@ -38,12 +38,7 @@
<i class="form-icon mdi mdi-magnify mdi-18px" />
</div>
</div>
<WorkspaceConnectPanel
v-if="!workspace.connected"
class="workspace-explorebar-body"
:connection="connection"
/>
<div v-else class="workspace-explorebar-body">
<div class="workspace-explorebar-body">
<WorkspaceExploreBarSchema
v-for="db of workspace.structure"
:key="db.name"
@@ -52,6 +47,7 @@
@show-schema-context="openSchemaContext"
@show-table-context="openTableContext"
@show-misc-context="openMiscContext"
@show-misc-folder-context="openMiscFolderContext"
/>
</div>
</div>
@@ -90,6 +86,12 @@
@close="hideCreateFunctionModal"
@open-create-function-editor="openCreateFunctionEditor"
/>
<ModalNewTriggerFunction
v-if="isNewTriggerFunctionModal"
:workspace="workspace"
@close="hideCreateTriggerFunctionModal"
@open-create-function-editor="openCreateTriggerFunctionEditor"
/>
<ModalNewScheduler
v-if="isNewSchedulerModal"
:workspace="workspace"
@@ -106,6 +108,7 @@
@show-create-trigger-modal="showCreateTriggerModal"
@show-create-routine-modal="showCreateRoutineModal"
@show-create-function-modal="showCreateFunctionModal"
@show-create-trigger-function-modal="showCreateTriggerFunctionModal"
@show-create-scheduler-modal="showCreateSchedulerModal"
@reload="refresh"
/>
@@ -123,6 +126,18 @@
@close-context="closeMiscContext"
@reload="refresh"
/>
<MiscFolderContext
v-if="isMiscFolderContext"
:selected-misc="selectedMisc"
:context-event="miscContextEvent"
@show-create-trigger-modal="showCreateTriggerModal"
@show-create-routine-modal="showCreateRoutineModal"
@show-create-function-modal="showCreateFunctionModal"
@show-create-trigger-function-modal="showCreateTriggerFunctionModal"
@show-create-scheduler-modal="showCreateSchedulerModal"
@close-context="closeMiscFolderContext"
@reload="refresh"
/>
</div>
</template>
@@ -136,33 +151,35 @@ import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema';
import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext';
import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext';
import ModalNewSchema from '@/components/ModalNewSchema';
import ModalNewTable from '@/components/ModalNewTable';
import ModalNewView from '@/components/ModalNewView';
import ModalNewTrigger from '@/components/ModalNewTrigger';
import ModalNewRoutine from '@/components/ModalNewRoutine';
import ModalNewFunction from '@/components/ModalNewFunction';
import ModalNewTriggerFunction from '@/components/ModalNewTriggerFunction';
import ModalNewScheduler from '@/components/ModalNewScheduler';
export default {
name: 'WorkspaceExploreBar',
components: {
WorkspaceConnectPanel,
WorkspaceExploreBarSchema,
DatabaseContext,
TableContext,
MiscContext,
MiscFolderContext,
ModalNewSchema,
ModalNewTable,
ModalNewView,
ModalNewTrigger,
ModalNewRoutine,
ModalNewFunction,
ModalNewTriggerFunction,
ModalNewScheduler
},
props: {
@@ -179,6 +196,7 @@ export default {
isNewTriggerModal: false,
isNewRoutineModal: false,
isNewFunctionModal: false,
isNewTriggerFunctionModal: false,
isNewSchedulerModal: false,
localWidth: null,
@@ -187,6 +205,7 @@ export default {
isDatabaseContext: false,
isTableContext: false,
isMiscContext: false,
isMiscFolderContext: false,
databaseContextEvent: null,
tableContextEvent: null,
@@ -209,6 +228,9 @@ export default {
},
connectionName () {
return this.getConnectionName(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
}
},
watch: {
@@ -320,11 +342,20 @@ export default {
this.miscContextEvent = payload.event;
this.isMiscContext = true;
},
openMiscFolderContext (payload) {
this.selectedMisc = payload.type;
this.miscContextEvent = payload.event;
this.isMiscFolderContext = true;
},
closeMiscContext () {
this.isMiscContext = false;
},
closeMiscFolderContext () {
this.isMiscFolderContext = false;
},
showCreateViewModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewViewModal = true;
},
hideCreateViewModal () {
@@ -348,6 +379,7 @@ export default {
},
showCreateTriggerModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewTriggerModal = true;
},
hideCreateTriggerModal () {
@@ -363,7 +395,8 @@ export default {
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, trigger: payload.name });
const triggerName = this.customizations.triggerTableInName ? `${payload.table}.${payload.name}` : payload.name;
this.changeBreadcrumbs({ schema: this.selectedDatabase, trigger: triggerName });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
@@ -371,6 +404,7 @@ export default {
},
showCreateRoutineModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewRoutineModal = true;
},
hideCreateRoutineModal () {
@@ -394,13 +428,23 @@ export default {
},
showCreateFunctionModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewFunctionModal = true;
},
hideCreateFunctionModal () {
this.isNewFunctionModal = false;
},
showCreateTriggerFunctionModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewTriggerFunctionModal = true;
},
hideCreateTriggerFunctionModal () {
this.isNewTriggerFunctionModal = false;
},
showCreateSchedulerModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewSchedulerModal = true;
},
hideCreateSchedulerModal () {
@@ -422,6 +466,22 @@ export default {
else
this.addNotification({ status: 'error', message: response });
},
async openCreateTriggerFunctionEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Functions.createTriggerFunction(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, triggerFunction: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
},
async openCreateSchedulerEditor (payload) {
const params = {
uid: this.connection.uid,
@@ -451,6 +511,11 @@ export default {
height: calc(100vh - #{$excluding-size});
cursor: ew-resize;
z-index: 99;
transition: background 0.2s;
&:hover {
background: rgba($primary-color, 50%);
}
}
.workspace-explorebar {

View File

@@ -20,7 +20,8 @@
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ deleteMessage }}
<i class="mdi mdi-24px mdi-delete mr-1" />
<span class="cut-text">{{ deleteMessage }}</span>
</div>
</template>
<div slot="body">
@@ -32,6 +33,7 @@
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localElement"
:client="workspace.client"
@confirm="runElement"
@close="hideAskParamsModal"
/>
@@ -82,6 +84,7 @@ export default {
case 'procedure':
return this.$t('message.deleteRoutine');
case 'function':
case 'triggerFunction':
return this.$t('message.deleteFunction');
case 'scheduler':
return this.$t('message.deleteScheduler');
@@ -133,6 +136,7 @@ export default {
});
break;
case 'function':
case 'triggerFunction':
res = await Functions.dropFunction({
uid: this.selectedWorkspace,
func: this.selectedMisc.name
@@ -205,13 +209,13 @@ export default {
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`;
sql = `CALL ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`;
sql = `CALL \`${this.localElement.name}\`(${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
@@ -247,9 +251,11 @@ export default {
switch (this.workspace.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.localElement.name} ${params.join(',')}`;
break;

View File

@@ -0,0 +1,97 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<div
v-if="selectedMisc === 'trigger'"
class="context-element"
@click="$emit('show-create-trigger-modal')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $t('message.createNewTrigger') }}</span>
</div>
<div
v-if="selectedMisc === 'procedure'"
class="context-element"
@click="$emit('show-create-routine-modal')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle text-light pr-1" /> {{ $t('message.createNewRoutine') }}</span>
</div>
<div
v-if="selectedMisc === 'function'"
class="context-element"
@click="$emit('show-create-function-modal')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box text-light pr-1" /> {{ $t('message.createNewFunction') }}</span>
</div>
<div
v-if="selectedMisc === 'triggerFunction'"
class="context-element"
@click="$emit('show-create-trigger-function-modal')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-cog-clockwise text-light pr-1" /> {{ $t('message.createNewFunction') }}</span>
</div>
<div
v-if="selectedMisc === 'scheduler'"
class="context-element"
@click="$emit('show-create-scheduler-modal')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $t('message.createNewScheduler') }}</span>
</div>
</BaseContextMenu>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
export default {
name: 'WorkspaceExploreBarMiscContext',
components: {
BaseContextMenu
},
props: {
contextEvent: MouseEvent,
selectedMisc: String
},
data () {
return {
localElement: {}
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},
hideDeleteModal () {
this.isDeleteModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
}
}
};
</script>

View File

@@ -39,7 +39,11 @@
<div v-if="filteredTriggers.length && customizations.triggers" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}">
<summary
class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}"
@contextmenu.prevent="showMiscFolderContext($event, 'trigger')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
{{ $tc('word.trigger', 2) }}
</summary>
@@ -67,7 +71,11 @@
<div v-if="filteredProcedures.length && customizations.routines" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}">
<summary
class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}"
@contextmenu.prevent="showMiscFolderContext($event, 'procedure')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
{{ $tc('word.storedRoutine', 2) }}
</summary>
@@ -75,8 +83,8 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="procedure of filteredProcedures"
:key="procedure.name"
v-for="(procedure, i) of filteredProcedures"
:key="`${procedure.name}-${i}`"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@click="setBreadcrumbs({schema: database.name, procedure: procedure.name})"
@@ -93,9 +101,45 @@
</details>
</div>
<div v-if="filteredTriggerFunctions.length && customizations.triggerFunctions" class="database-misc">
<details class="accordion">
<summary
class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction}"
@contextmenu.prevent="showMiscFolderContext($event, 'triggerFunction')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-refresh mr-1" />
{{ $tc('word.triggerFunction', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="(func, i) of filteredTriggerFunctions"
:key="`${func.name}-${i}`"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}"
@click="setBreadcrumbs({schema: database.name, triggerFunction: func.name})"
@contextmenu.prevent="showMiscContext($event, {...func, type: 'triggerFunction'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-cog-clockwise mdi-18px mr-1" />
<span v-html="highlightWord(func.name)" />
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="filteredFunctions.length && customizations.functions" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}">
<summary
class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}"
@contextmenu.prevent="showMiscFolderContext($event, 'function')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
{{ $tc('word.function', 2) }}
</summary>
@@ -103,8 +147,8 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="func of filteredFunctions"
:key="func.name"
v-for="(func, i) of filteredFunctions"
:key="`${func.name}-${i}`"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@click="setBreadcrumbs({schema: database.name, function: func.name})"
@@ -123,7 +167,11 @@
<div v-if="filteredSchedulers.length && customizations.schedulers" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}">
<summary
class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}"
@contextmenu.prevent="showMiscFolderContext($event, 'scheduler')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
{{ $tc('word.scheduler', 2) }}
</summary>
@@ -189,6 +237,11 @@ export default {
filteredFunctions () {
return this.database.functions.filter(func => func.name.search(this.searchTerm) >= 0);
},
filteredTriggerFunctions () {
return this.database.triggerFunctions
? this.database.triggerFunctions.filter(func => func.name.search(this.searchTerm) >= 0)
: [];
},
filteredSchedulers () {
return this.database.schedulers.filter(scheduler => scheduler.name.search(this.searchTerm) >= 0);
},
@@ -218,7 +271,7 @@ export default {
}),
formatBytes,
async selectSchema (schema) {
if (!this.loadedSchemas.has(schema)) {
if (!this.loadedSchemas.has(schema) && !this.isLoading) {
this.isLoading = true;
await this.refreshSchema({ uid: this.connection.uid, schema });
this.isLoading = false;
@@ -238,6 +291,11 @@ export default {
this.setBreadcrumbs({ schema: this.database.name, [misc.type]: misc.name });
this.$emit('show-misc-context', { event, misc });
},
showMiscFolderContext (event, type) {
this.selectSchema(this.database.name);
this.setBreadcrumbs({ schema: this.database.name, type });
this.$emit('show-misc-folder-context', { event, type });
},
piePercentage (val) {
const perc = val / this.maxSize * 100;
if (this.applicationTheme === 'dark')
@@ -311,7 +369,7 @@ export default {
.database-name,
.misc-name {
&:hover {
border-radius: 2px;
border-radius: $border-radius;
}
}
@@ -320,7 +378,7 @@ export default {
position: relative;
&:hover {
border-radius: 2px;
border-radius: $border-radius;
}
}

View File

@@ -42,6 +42,13 @@
>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div>
<div
v-if="workspace.customizations.triggerFunctionAdd"
class="context-element"
@click="showCreateTriggerFunctionModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-cog-clockwise pr-1" /> {{ $tc('word.triggerFunction', 1) }}</span>
</div>
<div
v-if="workspace.customizations.schedulerAdd"
class="context-element"
@@ -69,7 +76,8 @@
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteSchema') }}
<i class="mdi mdi-24px mdi-database-remove mr-1" />
<span class="cut-text">{{ $t('message.deleteSchema') }}</span>
</div>
</template>
<div slot="body">
@@ -139,6 +147,9 @@ export default {
showCreateFunctionModal () {
this.$emit('show-create-function-modal');
},
showCreateTriggerFunctionModal () {
this.$emit('show-create-trigger-function-modal');
},
showCreateSchedulerModal () {
this.$emit('show-create-scheduler-modal');
},

View File

@@ -3,6 +3,13 @@
:context-event="contextEvent"
@close-context="closeContext"
>
<div
v-if="selectedTable.type === 'table'"
class="context-element"
@click="duplicateTable"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-multiple text-light pr-1" /> {{ $t('message.duplicateTable') }}</span>
</div>
<div
v-if="selectedTable.type === 'table'"
class="context-element"
@@ -21,7 +28,7 @@
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-off mr-1" /> {{ $t('message.emptyTable') }}
<i class="mdi mdi-24px mdi-table-off mr-1" /> <span class="cut-text">{{ $t('message.emptyTable') }}</span>
</div>
</template>
<div slot="body">
@@ -37,7 +44,8 @@
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-remove mr-1" /> {{ selectedTable.type === 'table' ? $t('message.deleteTable') : $t('message.deleteView') }}
<i class="mdi mdi-24px mdi-table-remove mr-1" />
<span class="cut-text">{{ selectedTable.type === 'table' ? $t('message.deleteTable') : $t('message.deleteView') }}</span>
</div>
</template>
<div slot="body">
@@ -104,6 +112,24 @@ export default {
closeContext () {
this.$emit('close-context');
},
async duplicateTable () {
try {
const { status, response } = await Tables.duplicateTable({
uid: this.selectedWorkspace,
table: this.selectedTable.name
});
if (status === 'success') {
this.closeContext();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
async emptyTable () {
try {
const { status, response } = await Tables.truncateTable({

View File

@@ -8,7 +8,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-key-link mr-1" /> {{ $t('word.foreignKeys') }} "{{ table }}"
<i class="mdi mdi-24px mdi-key-link mr-1" />
<span class="cut-text">{{ $t('word.foreignKeys') }} "{{ table }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -18,8 +19,8 @@
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addForeign">
<i class="mdi mdi-24px mdi-link-plus mr-1" />
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-link-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
@@ -27,8 +28,8 @@
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
@@ -361,7 +362,7 @@ export default {
<style lang="scss" scoped>
.tile {
border-radius: 2px;
border-radius: $border-radius;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;

View File

@@ -7,7 +7,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" /> {{ $t('word.options') }} "{{ localOptions.name }}"
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span class="cut-text">{{ $t('word.options') }} "{{ localOptions.name }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -26,7 +27,19 @@
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
@@ -65,6 +78,12 @@
class="form-select text-uppercase"
style="width: 0;"
>
<option v-if="localOptions.returns === 'VOID'">
VOID
</option>
<option v-if="!isInDataTypes">
{{ localOptions.returns }}
</option>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
@@ -81,6 +100,7 @@
</optgroup>
</select>
<input
v-if="customizations.parametersLength"
v-model="optionsProxy.returnsLength"
class="form-input"
type="number"
@@ -89,7 +109,7 @@
</div>
</div>
</div>
<div class="form-group">
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
@@ -112,7 +132,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
@@ -125,7 +145,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
@@ -159,6 +179,19 @@ export default {
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
},
isInDataTypes () {
let typeNames = [];
for (const group of this.workspace.dataTypes) {
typeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
}
return typeNames.includes(this.localOptions.returns);
}
},
created () {

View File

@@ -8,7 +8,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> {{ $t('word.parameters') }} "{{ func }}"
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ func }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -18,8 +19,8 @@
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<i class="mdi mdi-24px mdi-plus mr-1" />
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
@@ -27,8 +28,8 @@
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
@@ -49,7 +50,7 @@
<div class="tile-title">
{{ param.name }}
</div>
<small class="tile-subtitle text-gray">{{ param.type }}{{ param.length ? `(${param.length})` : '' }}</small>
<small class="tile-subtitle text-gray">{{ param.type }}{{ param.length ? `(${param.length})` : '' }} · {{ param.context }}</small>
</div>
<div class="tile-action">
<button
@@ -106,7 +107,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3">
{{ $t('word.length') }}
</label>
@@ -119,6 +120,37 @@
>
</div>
</div>
<div v-if="customizations.functionContext" class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
</label>
<div class="column">
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
name="context"
value="IN"
> <i class="form-icon" /> IN
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="OUT"
name="context"
> <i class="form-icon" /> OUT
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="INOUT"
name="context"
> <i class="form-icon" /> INOUT
</label>
</div>
</div>
</form>
<div v-if="!parametersProxy.length" class="empty">
<div class="empty-icon">
@@ -168,6 +200,9 @@ export default {
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
@@ -239,7 +274,7 @@ export default {
<style lang="scss" scoped>
.tile {
border-radius: 2px;
border-radius: $border-radius;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;

View File

@@ -8,7 +8,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" /> {{ $t('word.indexes') }} "{{ table }}"
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" />
<span class="cut-text">{{ $t('word.indexes') }} "{{ table }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -18,8 +19,8 @@
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addIndex">
<i class="mdi mdi-24px mdi-key-plus mr-1" />
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-key-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
@@ -27,8 +28,8 @@
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
@@ -246,7 +247,7 @@ export default {
<style lang="scss" scoped>
.tile {
border-radius: 2px;
border-radius: $border-radius;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;

View File

@@ -7,7 +7,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" /> {{ $t('word.options') }} "{{ table }}"
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span class="cut-text">{{ $t('word.options') }} "{{ table }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -47,6 +48,7 @@
v-model="optionsProxy.autoIncrement"
class="form-input"
type="number"
:disabled="optionsProxy.autoIncrement === null"
>
</div>
</div>

View File

@@ -7,7 +7,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" /> {{ $t('word.options') }} "{{ localOptions.name }}"
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span class="cut-text">{{ $t('word.options') }} "{{ localOptions.name }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -26,7 +27,19 @@
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
@@ -54,7 +67,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
@@ -77,7 +90,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.procedureDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
@@ -90,7 +103,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
@@ -124,6 +137,9 @@ export default {
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
}
},
created () {

View File

@@ -8,7 +8,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> {{ $t('word.parameters') }} "{{ routine }}"
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ routine }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -18,8 +19,8 @@
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<i class="mdi mdi-24px mdi-plus mr-1" />
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
@@ -27,8 +28,8 @@
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
@@ -106,7 +107,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3">
{{ $t('word.length') }}
</label>
@@ -119,7 +120,7 @@
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.procedureContext" class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
</label>
@@ -199,6 +200,9 @@ export default {
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
@@ -235,10 +239,10 @@ export default {
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`,
type: 'INT',
name: `param${this.i++}`,
type: this.workspace.dataTypes[0].types[0].name,
context: 'IN',
length: 10
length: ''
}];
if (this.parametersProxy.length === 1)
@@ -270,7 +274,7 @@ export default {
<style lang="scss" scoped>
.tile {
border-radius: 2px;
border-radius: $border-radius;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;

View File

@@ -7,7 +7,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-timer mr-1" /> {{ $t('word.timing') }} "{{ localOptions.name }}"
<i class="mdi mdi-24px mdi-timer mr-1" />
<span class="cut-text">{{ $t('word.timing') }} "{{ localOptions.name }}"</span>
</div>
</template>
<div :slot="'body'">

View File

@@ -7,10 +7,11 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
@@ -18,8 +19,8 @@
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
<div class="divider-vert py-3" />
@@ -29,24 +30,24 @@
:title="$t('message.addNewField')"
@click="addField"
>
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm"
:title="$t('message.manageIndexes')"
@click="showIntdexesModal"
>
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" />
<span>{{ $t('word.indexes') }}</span>
<i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showForeignModal">
<i class="mdi mdi-24px mdi-key-link mr-1" />
<span>{{ $t('word.foreignKeys') }}</span>
<i class="mdi mdi-24px mdi-key-link ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
<i class="mdi mdi-24px mdi-cogs ml-1" />
</button>
</div>
</div>
@@ -68,6 +69,7 @@
@remove-field="removeField"
@add-new-index="addNewIndex"
@add-to-index="addToIndex"
@rename-field="renameField"
/>
</div>
<WorkspacePropsOptionsModal
@@ -149,6 +151,7 @@ export default {
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
workspace () {
@@ -162,7 +165,7 @@ export default {
return this.getDatabaseVariable(this.connection.uid, 'default_storage_engine').value || '';
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.table;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -199,6 +202,12 @@ export default {
this.setUnsavedChanges(val);
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -224,6 +233,20 @@ export default {
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
this.originalFields = response.map(field => {
if (field.autoIncrement)
field.defaultType = 'autoincrement';
else if (field.default === null)
field.defaultType = 'noval';
else if (field.default === 'NULL')
field.defaultType = 'null';
else if (isNaN(+field.default) && field.default.charAt(0) !== '\'')
field.defaultType = 'expression';
else {
field.defaultType = 'custom';
if (isNaN(+field.default) && !field.default.includes('\''))
field.default = `'${field.default}'`;
}
return { ...field, _id: uidGen() };
});
this.localFields = JSON.parse(JSON.stringify(this.originalFields));
@@ -457,6 +480,20 @@ export default {
scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20);
},
renameField (payload) {
this.localIndexes = this.localIndexes.map(index => {
const fi = index.fields.findIndex(field => field === payload.old);
if (fi !== -1)
index.fields[fi] = payload.new;
return index;
});
this.localKeyUsage = this.localKeyUsage.map(key => {
if (key.field === payload.old)
key.field = payload.new;
return key;
});
},
removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid);
},
@@ -504,6 +541,15 @@ export default {
},
foreignsUpdate (foreigns) {
this.localKeyUsage = foreigns;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -7,10 +7,11 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
@@ -18,8 +19,8 @@
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
<div class="divider-vert py-3" />
@@ -29,16 +30,16 @@
:disabled="isChanged"
@click="runFunctionCheck"
>
<i class="mdi mdi-24px mdi-play mr-1" />
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
<i class="mdi mdi-24px mdi-dots-horizontal ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
<i class="mdi mdi-24px mdi-cogs ml-1" />
</button>
</div>
</div>
@@ -47,7 +48,7 @@
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
<QueryEditor
v-if="isSelected"
v-show="isSelected"
ref="queryEditor"
:value.sync="localFunction.sql"
:workspace="workspace"
@@ -73,6 +74,7 @@
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localFunction"
:client="workspace.client"
@confirm="runFunction"
@close="hideAskParamsModal"
/>
@@ -119,13 +121,14 @@ export default {
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.function;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -134,7 +137,9 @@ export default {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction ? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``) : true;
return this.originalFunction
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
schemaTables () {
const schemaTables = this.workspace.structure
@@ -170,6 +175,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -280,9 +291,11 @@ export default {
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${this.originalFunction.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break;
@@ -309,6 +322,15 @@ export default {
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -7,10 +7,11 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
@@ -18,8 +19,8 @@
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
<div class="divider-vert py-3" />
@@ -29,16 +30,16 @@
:disabled="isChanged"
@click="runRoutineCheck"
>
<i class="mdi mdi-24px mdi-play mr-1" />
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
<i class="mdi mdi-24px mdi-dots-horizontal ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
<i class="mdi mdi-24px mdi-cogs ml-1" />
</button>
</div>
</div>
@@ -74,6 +75,7 @@
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localRoutine"
:client="workspace.client"
@confirm="runRoutine"
@close="hideAskParamsModal"
/>
@@ -120,13 +122,14 @@ export default {
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.routine;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -171,6 +174,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -281,13 +290,13 @@ export default {
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`;
sql = `CALL ${this.originalRoutine.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.originalRoutine.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`;
sql = `CALL \`${this.originalRoutine.name}\`(${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
@@ -309,6 +318,15 @@ export default {
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -7,10 +7,11 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
@@ -18,14 +19,14 @@
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showTimingModal">
<i class="mdi mdi-24px mdi-timer mr-1" />
<span>{{ $t('word.timing') }}</span>
<i class="mdi mdi-24px mdi-timer ml-1" />
</button>
</div>
</div>
@@ -169,13 +170,14 @@ export default {
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.scheduler;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -220,6 +222,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -310,6 +318,15 @@ export default {
},
timingUpdate (options) {
this.localScheduler = options;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -7,10 +7,11 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
@@ -18,8 +19,8 @@
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
@@ -36,7 +37,7 @@
>
</div>
</div>
<div class="column col-auto">
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
@@ -66,7 +67,7 @@
</div>
</div>
</div>
<div class="columns">
<fieldset class="columns" :disabled="customizations.triggerOnlyRename">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label>
@@ -81,19 +82,33 @@
<div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label>
<div class="input-group">
<select v-model="localTrigger.event1" class="form-select">
<select v-model="localTrigger.activation" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select v-model="localTrigger.event2" class="form-select">
<option>INSERT</option>
<option>UPDATE</option>
<option>DELETE</option>
<select
v-if="!customizations.triggerMultipleEvents"
v-model="localTrigger.event"
class="form-select"
>
<option v-for="event in Object.keys(localEvents)" :key="event">
{{ event }}
</option>
</select>
<div v-if="customizations.triggerMultipleEvents" class="px-4">
<label
v-for="event in Object.keys(localEvents)"
:key="event"
class="form-checkbox form-inline"
@change.prevent="changeEvents(event)"
>
<input :checked="localEvents[event]" type="checkbox"><i class="form-icon" /> {{ event }}
</label>
</div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
@@ -135,18 +150,23 @@ export default {
localTrigger: { sql: '' },
lastTrigger: null,
sqlProxy: '',
editorHeight: 300
editorHeight: 300,
localEvents: { INSERT: false, UPDATE: false, DELETE: false }
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.trigger;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -191,6 +211,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -201,6 +227,10 @@ export default {
async getTriggerData () {
if (!this.trigger) return;
Object.keys(this.localEvents).forEach(event => {
this.localEvents[event] = false;
});
this.localTrigger = { sql: '' };
this.isLoading = true;
@@ -216,6 +246,12 @@ export default {
this.originalTrigger = response;
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
this.sqlProxy = this.localTrigger.sql;
if (this.customizations.triggerMultipleEvents) {
this.originalTrigger.event.forEach(e => {
this.localEvents[e] = true;
});
}
}
else
this.addNotification({ status: 'error', message: response });
@@ -227,6 +263,16 @@ export default {
this.resizeQueryEditor();
this.isLoading = false;
},
changeEvents (event) {
if (this.customizations.triggerMultipleEvents) {
this.localEvents[event] = !this.localEvents[event];
this.localTrigger.event = [];
for (const key in this.localEvents) {
if (this.localEvents[key])
this.localTrigger.event.push(key);
}
}
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
@@ -249,7 +295,8 @@ export default {
if (oldName !== this.localTrigger.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, trigger: this.localTrigger.name });
const triggerName = this.customizations.triggerTableInName ? `${this.localTrigger.table}.${this.localTrigger.name}` : this.localTrigger.name;
this.changeBreadcrumbs({ schema: this.schema, trigger: triggerName });
}
this.getTriggerData();
@@ -266,6 +313,16 @@ export default {
clearChanges () {
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
Object.keys(this.localEvents).forEach(event => {
this.localEvents[event] = false;
});
if (this.customizations.triggerMultipleEvents) {
this.originalTrigger.event.forEach(e => {
this.localEvents[e] = true;
});
}
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
@@ -274,6 +331,15 @@ export default {
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -0,0 +1,315 @@
<template>
<div class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
</button>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localFunction.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspacePropsTriggerFunctionOptionsModal
v-if="isOptionsModal"
:local-options="localFunction"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localFunction"
:client="workspace.client"
@confirm="runFunction"
@close="hideAskParamsModal"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsTriggerFunctionOptionsModal from '@/components/WorkspacePropsTriggerFunctionOptionsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspacePropsTabTriggerFunction',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsTriggerFunctionOptionsModal,
ModalAskParameters
},
props: {
connection: Object,
function: String
},
data () {
return {
tabUid: 'prop',
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
localFunction: { sql: '' },
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.function;
},
schema () {
return this.workspace.breadcrumbs.schema;
},
isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
async function () {
if (this.isSelected) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
}
},
async isSelected (val) {
if (val && this.lastFunction !== this.function) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
}
},
isChanged (val) {
if (this.isSelected && this.lastFunction === this.function && this.function !== null)
this.setUnsavedChanges(val);
}
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
async getFunctionData () {
if (!this.function) return;
this.isLoading = true;
this.localFunction = { sql: '' };
const params = {
uid: this.connection.uid,
schema: this.schema,
func: this.workspace.breadcrumbs.triggerFunction
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success') {
this.originalFunction = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => {
param._id = uidGen();
return param;
})];
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.sqlProxy = this.localFunction.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor();
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
func: {
...this.localFunction,
oldName: this.originalFunction.name
}
};
try {
const { status, response } = await Functions.alterTriggerFunction(params);
if (status === 'success') {
const oldName = this.originalFunction.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localFunction.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, function: this.localFunction.name });
}
this.getFunctionData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
optionsUpdate (options) {
this.localFunction = options;
},
parametersUpdate (parameters) {
this.localFunction = { ...this.localFunction, parameters };
},
runFunctionCheck () {
if (this.localFunction.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql;
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${this.originalFunction.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

View File

@@ -7,10 +7,11 @@
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
@@ -18,8 +19,8 @@
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
@@ -201,13 +202,14 @@ export default {
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
return this.workspace.selected_tab === 'prop' && this.selectedWorkspace === this.workspace.uid && this.view;
},
schema () {
return this.workspace.breadcrumbs.schema;
@@ -245,6 +247,12 @@ export default {
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
@@ -327,6 +335,15 @@ export default {
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};

View File

@@ -42,6 +42,13 @@
</div>
</div>
</div>
<div v-if="customizations.tableArray" class="th">
<div class="column-resizable">
<div class="table-column-title">
{{ $t('word.array') }}
</div>
</div>
</div>
<div class="th">
<div class="column-resizable">
<div class="table-column-title">
@@ -108,6 +115,7 @@
:data-types="dataTypes"
:customizations="customizations"
@contextmenu="contextMenu"
@rename-field="$emit('rename-field', $event)"
/>
</draggable>
</div>

View File

@@ -60,6 +60,9 @@
class="form-select editable-field small-select text-uppercase"
@blur="editOFF"
>
<option v-if="!isInDataTypes">
{{ row.type }}
</option>
<optgroup
v-for="group in dataTypes"
:key="group.group"
@@ -68,7 +71,7 @@
<option
v-for="type in group.types"
:key="type.name"
:selected="localRow.type.toUpperCase() === type.name"
:selected="localRow.type === type.name"
:value="type.name"
>
{{ type.name }}
@@ -76,6 +79,16 @@
</optgroup>
</select>
</div>
<div
v-if="customizations.tableArray"
class="td"
tabindex="0"
>
<label class="form-checkbox">
<input v-model="localRow.isArray" type="checkbox">
<i class="form-icon" />
</label>
</div>
<div class="td type-int" tabindex="0">
<template v-if="fieldType.length">
<span
@@ -83,8 +96,22 @@
class="cell-content"
@dblclick="editON($event, localLength, 'length')"
>
{{ localLength }}
<span v-if="localRow.enumValues">
{{ localRow.enumValues }}
</span>
<span v-else>
{{ localLength }}
</span>
</span>
<input
v-else-if="localRow.enumValues"
ref="editField"
v-model="editingContent"
type="text"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
<input
v-else
ref="editField"
@@ -205,7 +232,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.default') }} "{{ row.name }}"
<i class="mdi mdi-24px mdi-playlist-edit mr-1" />
<span class="cut-text">{{ $t('word.default') }} "{{ row.name }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -364,6 +392,18 @@ export default {
},
isNullable () {
return !this.indexes.some(index => ['PRIMARY'].includes(index.type));
},
isInDataTypes () {
let typeNames = [];
for (const group of this.dataTypes) {
const groupTypeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
typeNames = [...groupTypeNames, ...typeNames];
}
return typeNames.includes(this.row.type);
}
},
watch: {
@@ -410,40 +450,35 @@ export default {
});
this.defaultValue.onUpdate = this.localRow.onUpdate;
if (this.localRow.autoIncrement)
this.defaultValue.type = 'autoincrement';
else if (this.localRow.default === null)
this.defaultValue.type = 'noval';
else if (this.localRow.default === 'NULL')
this.defaultValue.type = 'null';
else if (this.localRow.default.match(/^'.*'$/g)) {
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default.replace(/(^')|('$)/g, '');
this.defaultValue.type = this.localRow.defaultType;
if (this.defaultValue.type === 'custom') {
this.defaultValue.custom = this.localRow.default
? this.localRow.default.includes('\'')
? this.localRow.default.split('\'')[1]
: this.localRow.default
: '';
}
else if (!isNaN(this.localRow.default.replace(/[:.-\s]/g, ''))) {
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default;
}
else {
this.defaultValue.type = 'expression';
if (this.defaultValue.type === 'expression')
this.defaultValue.expression = this.localRow.default;
}
},
updateRow () {
this.$emit('input', this.localRow);
},
editON (event, content, field) {
if (field === 'length') {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.editingField = 'numLength';
if (['string', 'other'].includes(this.fieldType.group)) this.editingField = 'charLength';
if (['time'].includes(this.fieldType.group)) this.editingField = 'datePrecision';
else if (['string', 'unknown'].includes(this.fieldType.group)) this.editingField = 'charLength';
else if (['other'].includes(this.fieldType.group)) this.editingField = 'enumValues';
else if (['time'].includes(this.fieldType.group)) this.editingField = 'datePrecision';
}
else
this.editingField = field;
this.editingContent = content;
this.originalContent = content;
if (this.localRow.enumValues && field === 'length') {
this.editingContent = this.localRow.enumValues;
this.originalContent = this.localRow.enumValues;
}
else {
this.editingContent = content;
this.originalContent = content;
}
const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
@@ -459,17 +494,22 @@ export default {
}
},
editOFF () {
if (this.editingField === 'name')
this.$emit('rename-field', { old: this.localRow[this.editingField], new: this.editingContent });
this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'type' && this.editingContent !== this.originalContent) {
this.localRow.numLength = null;
this.localRow.charLength = null;
this.localRow.datePrecision = null;
this.localRow.enumValues = '';
if (this.fieldType.length) {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.localRow.numLength = 11;
if (['string', 'other'].includes(this.fieldType.group)) this.localRow.charLength = 15;
if (['string'].includes(this.fieldType.group)) this.localRow.charLength = 15;
if (['time'].includes(this.fieldType.group)) this.localRow.datePrecision = 0;
if (['other'].includes(this.fieldType.group)) this.localRow.enumValues = '\'valA\',\'valB\'';
}
if (!this.fieldType.collation)
@@ -481,8 +521,7 @@ export default {
if (!this.fieldType.zerofill)
this.localRow.zerofill = false;
}
if (this.editingField === 'default') {
else if (this.editingField === 'default') {
switch (this.defaultValue.type) {
case 'autoincrement':
this.localRow.autoIncrement = true;
@@ -497,7 +536,7 @@ export default {
break;
case 'custom':
this.localRow.autoIncrement = false;
this.localRow.default = `'${this.defaultValue.custom}'`;
this.localRow.default = Number.isNaN(+this.defaultValue.custom) ? `'${this.defaultValue.custom}'` : this.defaultValue.custom;
break;
case 'expression':
this.localRow.autoIncrement = false;

View File

@@ -0,0 +1,125 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span class="cut-text">{{ $t('word.options') }} "{{ localOptions.name }}"</span>
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div v-if="customizations.triggerFunctionlanguages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.triggerFunctionlanguages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="optionsProxy.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="optionsProxy.comment"
class="form-input"
type="text"
>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsTriggerFunctionOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
},
isInDataTypes () {
let typeNames = [];
for (const group of this.workspace.dataTypes) {
typeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
}
return typeNames.includes(this.localOptions.returns);
}
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
if (!this.isTableNameValid)
this.optionsProxy.name = this.localOptions.name;
this.$emit('options-update', this.optionsProxy);
}
}
};
</script>

View File

@@ -1,5 +1,12 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div
v-show="isSelected"
class="workspace-query-tab column col-12 columns col-gapless no-outline"
tabindex="0"
@keydown.116="runQuery(query)"
@keydown.ctrl.87="clear"
@keydown.ctrl.119="beautify"
>
<div class="workspace-query-runner column col-12">
<QueryEditor
v-show="isSelected"
@@ -21,9 +28,49 @@
title="F5"
@click="runQuery(query)"
>
<i class="mdi mdi-24px mdi-play pr-1" />
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play" />
</button>
<button
class="btn btn-dark btn-sm"
:disabled="!query || isQuering"
title="CTRL+F8"
@click="beautify()"
>
<i class="mdi mdi-24px mdi-brush pr-1" />
<span>{{ $t('word.format') }}</span>
</button>
<button
class="btn btn-link btn-sm"
:disabled="!query || isQuering"
title="CTRL+W"
@click="clear()"
>
<i class="mdi mdi-24px mdi-delete-sweep pr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<div class="dropdown table-dropdown pr-2">
<button
:disabled="!results.length || isQuering"
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
tabindex="0"
>
<i class="mdi mdi-24px mdi-file-export mr-1" />
<span>{{ $t('word.export') }}</span>
<i class="mdi mdi-24px mdi-menu-down" />
</button>
<ul class="menu text-left">
<li class="menu-item">
<a class="c-hand" @click="downloadTable('json')">JSON</a>
</li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li>
</ul>
</div>
</div>
<div class="workspace-query-info">
<div
@@ -68,6 +115,7 @@
</template>
<script>
import { format } from 'sql-formatter';
import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
@@ -192,12 +240,36 @@ export default {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.resize();
},
onKey (e) {
if (this.isSelected && this.isWorkspaceSelected) {
e.stopPropagation();
if (e.key === 'F5')
this.runQuery(this.query);
beautify () {
if (this.$refs.queryEditor) {
let language = 'sql';
switch (this.workspace.client) {
case 'mysql':
language = 'mysql';
break;
case 'maria':
language = 'mariadb';
break;
case 'pg':
language = 'postgresql';
break;
}
const formattedQuery = format(this.query, {
language,
uppercase: true
});
this.$refs.queryEditor.editor.session.setValue(formattedQuery);
}
},
clear () {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue('');
this.clearTabData();
},
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, `${this.tab.type}-${this.tab.index}`);
}
}
};
@@ -212,11 +284,16 @@ export default {
.query-area-resizer {
position: absolute;
height: 5px;
height: 4px;
bottom: 40px;
width: 100%;
cursor: ns-resize;
z-index: 99;
transition: background 0.2s;
&:hover {
background: rgba($primary-color, 50%);
}
}
.workspace-query-runner-footer {
@@ -250,5 +327,4 @@ export default {
min-height: 200px;
}
}
</style>

View File

@@ -1,16 +1,20 @@
<template>
<div
ref="tableWrapper"
class="vscroll"
class="vscroll no-outline"
tabindex="0"
:style="{'height': resultsSize+'px'}"
@keyup.46="showDeleteConfirmModal"
>
<TableContext
v-if="isContext"
:context-event="contextEvent"
:selected-rows="selectedRows"
@delete-selected="deleteSelected"
@show-delete-modal="showDeleteConfirmModal"
@set-null="setNull"
@close-context="isContext = false"
@copy-cell="copyCell"
@copy-row="copyRow"
@close-context="closeContext"
/>
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
<li
@@ -75,6 +79,24 @@
</template>
</BaseVirtualScroll>
</div>
<ConfirmModal
v-if="isDeleteConfirmModal"
@confirm="deleteSelected"
@hide="hideDeleteConfirmModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" />
<span class="cut-text">{{ $tc('message.deleteRows', selectedRows.length) }}</span>
</div>
</template>
<div :slot="'body'">
<div class="mb-2">
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }}
</div>
</div>
</ConfirmModal>
</div>
</template>
@@ -85,14 +107,17 @@ import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext';
import ConfirmModal from '@/components/BaseConfirmModal';
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
export default {
name: 'WorkspaceQueryTable',
components: {
BaseVirtualScroll,
WorkspaceQueryTableRow,
TableContext
TableContext,
ConfirmModal
},
props: {
results: Array,
@@ -105,6 +130,7 @@ export default {
resultsSize: 1000,
localResults: [],
isContext: false,
isDeleteConfirmModal: false,
contextEvent: null,
selectedCell: null,
selectedRows: [],
@@ -116,24 +142,26 @@ export default {
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
pageSize: 'settings/getDataTabLimit'
}),
workspaceSchema () {
return this.getWorkspace(this.connUid).breadcrumbs.schema;
},
primaryField () {
const primaryFields = this.fields.filter(field => ['pri', 'uni'].includes(field.key));
const primaryFields = this.fields.filter(field => field.key === 'pri');
const uniqueFields = this.fields.filter(field => field.key === 'uni');
if (primaryFields.length > 1 || !primaryFields.length)
if ((primaryFields.length > 1 || !primaryFields.length) && (uniqueFields.length > 1 || !uniqueFields.length))
return false;
return primaryFields[0];
return primaryFields[0] || uniqueFields[0];
},
isSortable () {
return this.fields.every(field => field.name);
},
isHardSort () {
return this.mode === 'table' && this.localResults.length === 1000;
return this.mode === 'table' && this.localResults.length === this.pageSize;
},
sortedResults () {
if (this.currentSort && !this.isHardSort) {
@@ -295,8 +323,17 @@ export default {
},
updateField (payload, row) {
const orgRow = this.localResults.find(lr => lr._id === row._id);
delete row._id;
delete orgRow._id;
Object.keys(orgRow).forEach(key => { // remap the row
if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime
let datePrecision = '';
const precision = this.fields.find(field => field.name === key).datePrecision;
for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
orgRow[key] = moment(orgRow[key]).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
});
const params = {
primary: this.primaryField.name,
@@ -309,7 +346,19 @@ export default {
};
this.$emit('update-field', params);
},
closeContext () {
this.isContext = false;
},
showDeleteConfirmModal (e) {
if (e && e.path && ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.path[0].tagName))
return;
this.isDeleteConfirmModal = true;
},
hideDeleteConfirmModal () {
this.isDeleteConfirmModal = false;
},
deleteSelected () {
this.closeContext();
const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => {
delete row._id;
return row;
@@ -325,7 +374,6 @@ export default {
},
setNull () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id));
delete row._id;
const params = {
primary: this.primaryField.name,
@@ -339,6 +387,22 @@ export default {
};
this.$emit('update-field', params);
},
copyCell () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id));
const cellName = Object.keys(row).find(prop => [
this.selectedCell.field,
`${this.fields[0].table}.${this.selectedCell.field}`,
`${this.fields[0].tableAlias}.${this.selectedCell.field}`
].includes(prop));
const valueToCopy = row[cellName];
navigator.clipboard.writeText(valueToCopy);
},
copyRow () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id));
const rowToCopy = JSON.parse(JSON.stringify(row));
delete rowToCopy._id;
navigator.clipboard.writeText(JSON.stringify(rowToCopy));
},
applyUpdate (params) {
const { primary, id, field, table, content } = params;
@@ -363,15 +427,15 @@ export default {
this.selectedRows.push(row);
else {
const lastID = this.selectedRows.slice(-1)[0];
const lastIndex = this.localResults.findIndex(el => el._id === lastID);
const clickedIndex = this.localResults.findIndex(el => el._id === row);
const lastIndex = this.sortedResults.findIndex(el => el._id === lastID);
const clickedIndex = this.sortedResults.findIndex(el => el._id === row);
if (lastIndex > clickedIndex) {
for (let i = clickedIndex; i < lastIndex; i++)
this.selectedRows.push(this.localResults[i]._id);
this.selectedRows.push(this.sortedResults[i]._id);
}
else if (lastIndex < clickedIndex) {
for (let i = clickedIndex; i > lastIndex; i--)
this.selectedRows.push(this.localResults[i]._id);
this.selectedRows.push(this.sortedResults[i]._id);
}
}
}
@@ -390,6 +454,8 @@ export default {
sort (field) {
if (!this.isSortable) return;
this.selectedRows = [];
if (this.mode === 'query')
field = `${this.getTable(this.resultsetIndex)}.${field}`;
@@ -417,7 +483,7 @@ export default {
downloadTable (format, filename) {
if (!this.sortedResults) return;
const rows = [...this.sortedResults].map(row => {
const rows = JSON.parse(JSON.stringify(this.sortedResults)).map(row => {
delete row._id;
return row;
});

View File

@@ -3,6 +3,30 @@
:context-event="contextEvent"
@close-context="closeContext"
>
<div v-if="selectedRows.length === 1" class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ $t('word.copy') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div
v-if="selectedRows.length === 1"
class="context-element"
@click="copyCell"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ $tc('word.cell', 1) }}
</span>
</div>
<div
v-if="selectedRows.length === 1"
class="context-element"
@click="copyRow"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ $tc('word.row', 1) }}
</span>
</div>
</div>
</div>
<div
v-if="selectedRows.length === 1"
class="context-element"
@@ -17,64 +41,41 @@
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</span>
</div>
<ConfirmModal
v-if="isConfirmModal"
@confirm="deleteRows"
@hide="hideConfirmModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</div>
</template>
<div :slot="'body'">
<div class="mb-2">
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }}
</div>
</div>
</ConfirmModal>
</BaseContextMenu>
</template>
<script>
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspaceQueryTableContext',
components: {
BaseContextMenu,
ConfirmModal
BaseContextMenu
},
props: {
contextEvent: MouseEvent,
selectedRows: Array
},
data () {
return {
isConfirmModal: false
};
},
computed: {
},
methods: {
showConfirmModal () {
this.isConfirmModal = true;
},
hideConfirmModal () {
this.isConfirmModal = false;
this.$emit('show-delete-modal');
},
closeContext () {
this.$emit('close-context');
},
deleteRows () {
this.$emit('delete-selected');
this.closeContext();
},
setNull () {
this.$emit('set-null');
this.closeContext();
},
copyCell () {
this.$emit('copy-cell');
this.closeContext();
},
copyRow () {
this.$emit('copy-row');
this.closeContext();
}
}
};

View File

@@ -10,7 +10,7 @@
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
v-if="!isInlineEditor[cKey] && fields[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON($event, col, cKey)"
@@ -43,6 +43,16 @@
<option>true</option>
<option>false</option>
</select>
<select
v-else-if="enumArray"
v-model="editingContent"
class="form-select small-select editable-field"
@blur="editOFF"
>
<option v-for="value in enumArray" :key="value">
{{ value }}
</option>
</select>
<input
v-else
ref="editField"
@@ -64,7 +74,7 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.edit') }} "{{ editingField }}"
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> <span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -128,7 +138,8 @@
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.edit') }} "{{ editingField }}"
<i class="mdi mdi-24px mdi-playlist-edit mr-1" />
<span class="cut-text">{{ $t('word.edit') }} "{{ editingField }}"</span>
</div>
</template>
<div :slot="'body'">
@@ -182,7 +193,7 @@ import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT, HAS_TIMEZONE } from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
@@ -286,6 +297,9 @@ export default {
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType))
timeMask += 'X##';
return { type: 'text', mask: timeMask };
}
@@ -299,6 +313,9 @@ export default {
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType))
datetimeMask += 'X##';
return { type: 'text', mask: datetimeMask };
}
@@ -330,6 +347,11 @@ export default {
}
return false;
},
enumArray () {
if (this.fields[this.editingField] && this.fields[this.editingField].enumValues)
return this.fields[this.editingField].enumValues.replaceAll('\'', '').split(',');
return false;
}
},
watch: {

View File

@@ -11,9 +11,9 @@
title="F5"
@click="reloadTable"
>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-1" />
<span>{{ $t('word.refresh') }}</span>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh ml-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h ml-1" />
</button>
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
<i class="mdi mdi-24px mdi-menu-down" />
@@ -32,6 +32,44 @@
</div>
</div>
</div>
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0"
:disabled="isQuering || page === 1"
title="CTRL+ᐊ"
@click="pageChange('prev')"
>
<i class="mdi mdi-24px mdi-skip-previous" />
</button>
<div class="dropdown" :class="{'active': isPageMenu}">
<div @click="openPageMenu">
<div class="btn btn-dark btn-sm mr-0 no-radius dropdown-toggle text-bold px-3">
{{ page }}
</div>
<div class="menu px-3">
<span>{{ $t('message.pageNumber') }}</span>
<input
ref="pageSelect"
v-model="pageProxy"
type="number"
min="1"
class="form-input"
@blur="setPageNumber"
>
</div>
</div>
</div>
<button
class="btn btn-dark btn-sm mr-0"
:disabled="isQuering || (results.length && results[0].rows.length < limit)"
title="CTRL+ᐅ"
@click="pageChange('next')"
>
<i class="mdi mdi-24px mdi-skip-next" />
</button>
</div>
<div class="divider-vert py-3" />
<button
v-if="isTable"
@@ -39,18 +77,18 @@
:disabled="isQuering"
@click="showFakerModal"
>
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $t('message.tableFiller') }}</span>
<i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button>
<div class="dropdown export-dropdown pr-2">
<div class="dropdown table-dropdown pr-2">
<button
:disabled="isQuering"
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
tabindex="0"
>
<i class="mdi mdi-24px mdi-file-export mr-1" />
<span>{{ $t('word.export') }}</span>
<i class="mdi mdi-24px mdi-file-export ml-1" />
<i class="mdi mdi-24px mdi-menu-down" />
</button>
<ul class="menu text-left">
@@ -72,10 +110,10 @@
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ results[0].duration / 1000 }}s</b>
</div>
<div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
{{ $t('word.results') }}: <b>{{ results[0].rows.length | localeString }}</b>
</div>
<div v-if="results.length && results[0].rows && tableInfo && results[0].rows.length < tableInfo.rows">
{{ $t('word.total') }}: <b>{{ tableInfo.rows.toLocaleString() }}</b> <small>({{ $t('word.approximately') }})</small>
<div v-if="hasApproximately || (page > 1 && tableInfo.rows)">
{{ $t('word.total') }}: <b>{{ tableInfo.rows | localeString }}</b> <small>({{ $t('word.approximately') }})</small>
</div>
<div v-if="workspace.breadcrumbs.database">
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b>
@@ -134,6 +172,12 @@ export default {
ModalNewTableRow,
ModalFakerRows
},
filters: {
localeString (val) {
if (val)
return val.toLocaleString();
}
},
mixins: [tableTabs],
props: {
connection: Object,
@@ -143,19 +187,23 @@ export default {
return {
tabUid: 'data',
isQuering: false,
isPageMenu: false,
results: [],
lastTable: null,
isAddModal: false,
isFakerModal: false,
autorefreshTimer: 0,
refreshInterval: null,
sortParams: {}
sortParams: {},
page: 1,
pageProxy: 1
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
selectedWorkspace: 'workspaces/getSelected',
limit: 'settings/getDataTabLimit'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
@@ -179,17 +227,31 @@ export default {
catch (err) {
return { rows: 0 };
}
},
hasApproximately () {
return this.results.length &&
this.results[0].rows &&
this.tableInfo &&
this.results[0].rows.length === this.limit &&
this.results[0].rows.length < this.tableInfo.rows;
}
},
watch: {
table () {
if (this.isSelected) {
this.page = 1;
this.sortParams = {};
this.getTableData();
this.lastTable = this.table;
this.$refs.queryTable.resetSort();
}
},
page (val, oldVal) {
if (val && val > 0 && val !== oldVal) {
this.pageProxy = this.page;
this.getTableData();
}
},
isSelected (val) {
if (val && this.lastTable !== this.table) {
this.getTableData();
@@ -221,6 +283,8 @@ export default {
uid: this.connection.uid,
schema: this.schema,
table: this.workspace.breadcrumbs.table || this.workspace.breadcrumbs.view,
limit: this.limit,
page: this.page,
sortParams
};
@@ -248,6 +312,33 @@ export default {
this.sortParams = sortParams;
this.getTableData(sortParams);
},
openPageMenu () {
if (this.isQuering) return;
this.isPageMenu = true;
if (this.isPageMenu)
setTimeout(() => this.$refs.pageSelect.focus(), 20);
},
setPageNumber () {
this.isPageMenu = false;
if (this.pageProxy > 0)
this.page = this.pageProxy;
else
this.pageProxy = this.page;
},
pageChange (direction) {
if (this.isQuering) return;
if (direction === 'next' && (this.results.length && this.results[0].rows.length === this.limit)) {
if (!this.page)
this.page = 2;
else
this.page++;
}
else if (direction === 'prev' && this.page > 1)
this.page--;
},
showAddModal () {
this.isAddModal = true;
},
@@ -266,6 +357,13 @@ export default {
e.stopPropagation();
if (e.key === 'F5')
this.reloadTable();
if (e.ctrlKey) {
if (e.key === 'ArrowRight')
this.pageChange('next');
if (e.key === 'ArrowLeft')
this.pageChange('prev');
}
}
},
setRefreshInterval () {
@@ -285,10 +383,3 @@ export default {
}
};
</script>
<style lang="scss" scoped>
.export-dropdown {
.menu {
min-width: 100%;
}
}
</style>

View File

@@ -15,18 +15,25 @@
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item"
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="c-hand">{{ $t('word.general') }}</a>
<a class="tab-link">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="c-hand">{{ $t('word.ssl') }}</a>
<a class="tab-link">{{ $t('word.ssl') }}</a>
</li>
<li
class="tab-item"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="c-hand">{{ $t('word.sshTunnel') }}</a>
</li>
</ul>
</div>
@@ -208,7 +215,6 @@
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
@@ -231,6 +237,95 @@
:status="toast.status"
/>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsh') }}
</label>
</div>
<div class="col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="localConnection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isTesting || !localConnection.ssh">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.sshKey"
:message="$t('word.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div class="modal-footer text-light">
<button
class="btn btn-gray mr-2"
@@ -369,6 +464,9 @@ export default {
toggleSsl () {
this.localConnection.ssl = !this.localConnection.ssl;
},
toggleSsh () {
this.localConnection.ssh = !this.localConnection.ssh;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;

View File

@@ -5,7 +5,8 @@
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-server-plus mr-1" /> {{ $t('message.createNewConnection') }}
<i class="mdi mdi-24px mdi-server-plus mr-1" />
<span class="cut-text">{{ $t('message.createNewConnection') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
@@ -15,18 +16,25 @@
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item"
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="c-hand">{{ $t('word.general') }}</a>
<a class="tab-link">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="c-hand">{{ $t('word.ssl') }}</a>
<a class="tab-link">{{ $t('word.ssl') }}</a>
</li>
<li
class="tab-item"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="c-hand">{{ $t('word.sshTunnel') }}</a>
</li>
</ul>
</div>
@@ -212,7 +220,6 @@
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
@@ -235,6 +242,95 @@
:status="toast.status"
/>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsh') }}
</label>
</div>
<div class="col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="connection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isTesting || !connection.ssh">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="connection.sshKey"
:message="$t('word.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
</div>
</div>
<div class="modal-footer text-light">
@@ -293,8 +389,13 @@ export default {
cert: '',
key: '',
ca: '',
ciphers: ''
ciphers: '',
ssh: false,
sshHost: '',
sshUser: '',
sshPass: '',
sshKey: '',
sshPort: 22
},
toast: {
status: '',
@@ -343,6 +444,7 @@ export default {
else {
try {
const res = await Connection.makeTest(this.connection);
console.log(res.response);
if (res.status === 'error')
this.toast = { status: 'error', message: res.response.message };
else
@@ -391,6 +493,9 @@ export default {
toggleSsl () {
this.connection.ssl = !this.connection.ssl;
},
toggleSsh () {
this.connection.ssh = !this.connection.ssh;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;

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