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

Compare commits

...

245 Commits

Author SHA1 Message Date
60e5556a3e chore(release): 0.3.9 2021-11-14 10:51:45 +01:00
b3f10220b3 build: transparent background for appx icon 2021-11-12 13:12:05 +01:00
d19f475fc2 perf(UI): improved function and routine parameters modals 2021-11-12 12:22:39 +01:00
795db96319 Merge pull request #135 from Fabio286/dependabot/npm_and_yarn/marked-4.0.0
build(deps): bump marked from 3.0.8 to 4.0.0
2021-11-10 12:00:20 +01:00
b5fee79e90 refactor: minor changes to support marked 4.0.0 2021-11-10 11:48:06 +01:00
dependabot[bot]
5532ddbda9 build(deps): bump marked from 3.0.8 to 4.0.0
Bumps [marked](https://github.com/markedjs/marked) from 3.0.8 to 4.0.0.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v3.0.8...v4.0.0)

---
updated-dependencies:
- dependency-name: marked
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-08 19:06:33 +00:00
3369d3dc2d refactor: prefix to internal rows id 2021-11-08 15:12:37 +01:00
fd25f881f9 feat: schema size in explore bar 2021-11-06 16:36:54 +01:00
5ca3a22dc5 feat(MySQL): enable/disable schedulers from contextual menu 2021-11-06 12:21:33 +01:00
5c668249cf feat(MySQL): scheduler status indicator in explore bar 2021-11-05 18:23:02 +01:00
39b9a59143 fix(PostgreSQL): bigint fetched as string instead of number, closes #134 2021-11-05 11:41:12 +01:00
534659f9ae feat(PostgreSQL): enable/disable triggers from contextual menu 2021-11-04 21:54:42 +01:00
c00fd1381f fix: temporary solution on MacOS for unsigned app updates 2021-11-03 14:46:13 +01:00
17c6686163 Merge pull request #133 from Fabio286/dependabot/npm_and_yarn/webpack-dev-server-4.4.0
build(deps-dev): bump webpack-dev-server from 3.11.2 to 4.4.0
2021-11-02 22:23:56 +01:00
bc0c5a76ba refactor: minor changes to support webpack-dev-server 4.4.0 2021-11-02 22:23:10 +01:00
2b16c0ece4 Merge pull request #132 from Fabio286/dependabot/npm_and_yarn/sass-loader-12.3.0
build(deps-dev): bump sass-loader from 10.2.0 to 12.3.0
2021-11-02 22:02:07 +01:00
dependabot[bot]
ba1416dce2 build(deps-dev): bump webpack-dev-server from 3.11.2 to 4.4.0
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 3.11.2 to 4.4.0.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v3.11.2...v4.4.0)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-02 20:55:42 +00:00
13adf0a767 Merge pull request #131 from Fabio286/dependabot/npm_and_yarn/eslint-plugin-vue-8.0.3
build(deps-dev): bump eslint-plugin-vue from 7.20.0 to 8.0.3
2021-11-02 21:52:38 +01:00
cacab55f55 refactor: minor changes to support eslint-plugin-vue 8 2021-11-02 21:51:53 +01:00
dependabot[bot]
3189d625e3 build(deps-dev): bump sass-loader from 10.2.0 to 12.3.0
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 10.2.0 to 12.3.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v10.2.0...v12.3.0)

---
updated-dependencies:
- dependency-name: sass-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 19:07:14 +00:00
dependabot[bot]
f71ed39b88 build(deps-dev): bump eslint-plugin-vue from 7.20.0 to 8.0.3
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 7.20.0 to 8.0.3.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v7.20.0...v8.0.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-vue
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 19:05:26 +00:00
729aa9781a chore: update README.md and CONTRIBUTING.md 2021-10-31 16:07:53 +01:00
cd1ebacf89 Merge pull request #126 from toriphes/master
feat: added macos basic shortcusts and menu
2021-10-31 15:24:56 +01:00
a08074b446 Merge branch 'master' of https://github.com/Fabio286/antares into pr/toriphes/126 2021-10-31 15:14:49 +01:00
0cd182546b build: electron-webpack replacement (#130)
* some changes

* improvements and dedicated webpeck configs for render and main

* added debugging setup

* vscode main process debug config

* vue3 devtools
2021-10-31 10:36:45 +01:00
89fdd210ca fix: row selection problems after a deletion fail, closes #128 2021-10-29 22:43:22 +02:00
Giulio Ganci
e2cf6e8c21 Merge branch 'Fabio286:master' into master 2021-10-29 12:43:00 +02:00
Giulio Ganci
430490ad93 feat: added macos basic shortcusts and menu 2021-10-24 13:02:37 +02:00
Giulio Ganci
a35566f273 feat(UI): double click on the title bar will toggle window fullscreen size 2021-10-24 13:00:36 +02:00
3679121c25 Merge pull request #125 from toriphes/master
MacOS improvements
2021-10-24 10:32:46 +02:00
Giulio Ganci
7657d05edf feat(UI): improved topbar look&feel on MacOS 2021-10-23 18:15:32 +02:00
Giulio Ganci
1ddf8f0dbe fix: copy&paste and basic usability on macOS 2021-10-23 17:56:42 +02:00
9506bb862a chore(release): 0.3.8 2021-10-23 10:18:46 +02:00
4cfab365c2 build: added .nvmrc 2021-10-21 18:17:40 +02:00
8d0aa73d1e chore: update dependencies 2021-10-21 10:51:07 +02:00
30b487c37f refactor(UI): removed text from refresh buttons 2021-10-20 18:14:02 +02:00
aef17be36c fix(PostgreSQL): issue with uppercase characters in table field names 2021-10-19 17:42:31 +02:00
ea65d8eee7 Merge pull request #123 from toriphes/master
feat(UI): multi column table filters
2021-10-19 09:11:29 +02:00
Giulio Ganci
7dc33c78aa fix: regression during resize results table on filters change 2021-10-18 21:23:22 +02:00
Giulio Ganci
69cd083054 fix: query failure when a filter with a numeric value is used 2021-10-18 21:13:19 +02:00
Giulio Ganci
91788054e6 feat(UI): hide filter bar if there are no more rows in it 2021-10-18 21:11:09 +02:00
Giulio Ganci
968a67ce3d refactor(UI): removed duplicate add filter button 2021-10-18 19:15:37 +02:00
f9ee7d0450 perf(UI): resize results table on filters change 2021-10-18 11:58:29 +02:00
Giulio Ganci
0e15c39797 feat(UI): multi column table filters 2021-10-17 23:54:00 +02:00
adf407c1ba Merge pull request #122 from toriphes/master
feat(UI): ctrl|cmd+t, ctrl|cmd+w shortcut to open/close workspace tabs
2021-10-16 18:50:51 +02:00
8a86344484 refactor(UI): display new shortcuts in empty query tab 2021-10-16 18:46:17 +02:00
d2d0c3ca41 refactor(UI): changed query clear shortcut 2021-10-16 18:43:23 +02:00
Giulio Ganci
9046b858b1 feat(UI): ctrl|cmd+t, ctrl|cmd+w shortcut to open/close workspace tabs 2021-10-16 17:05:26 +02:00
46987faea8 Merge pull request #121 from datlechin/master
Add "passphrase" to Vietnamese translation
2021-10-16 14:18:23 +02:00
Ngô Quốc Đạt
dd25827e40 Add "passphrase" to Vietnamese translation 2021-10-16 18:52:39 +07:00
4069ade36d feat(UI): CTRL+A to select all result rows 2021-10-16 09:58:32 +02:00
c8594c0549 chore(release): 0.3.7 2021-10-08 09:13:23 +02:00
7359c3b5bd Merge pull request #107 from Fabio286/dependabot/npm_and_yarn/ssh2-promise-1.0.2
build(deps): bump ssh2-promise from 0.2.0 to 1.0.2
2021-10-07 15:27:40 +02:00
4195b8416f Create CONTRIBUTING.md 2021-10-07 15:15:12 +02:00
9407a29922 feat: support to SSH private keys with passphrase, closes #118 2021-10-07 14:58:31 +02:00
2fcd080bd4 fix(PostgreSQL): issue with uppercase characters in table and field names, closes #116 2021-10-06 12:08:37 +02:00
26446fb7ed fix: closing ask credential modal during a connection doesn't stops loading, closes #114 2021-10-05 18:20:38 +02:00
2480c76a08 chore: update README.md 2021-10-05 10:53:45 +02:00
1f0ec57789 Merge pull request #115 from IsamuSugi/Japanese-translation
Add new language: Japanese
2021-10-05 10:49:47 +02:00
isamu
76e5849c78 Add new language: Japanese 2021-10-05 17:22:07 +09:00
db1641b74f fix(UI): window reload pressing enter in schema creation modal, closes #113 2021-10-05 09:16:45 +02:00
165c54f663 Merge pull request #111 from datlechin/master
Add new language: Tiếng Việt
2021-10-05 09:04:19 +02:00
Ngo Quoc Dat
a5ca3ea204 Add new language: Tiếng Việt 2021-10-04 18:30:44 +07:00
dependabot[bot]
add95292ad build(deps): bump ssh2-promise from 0.2.0 to 1.0.2
Bumps [ssh2-promise](https://github.com/sanketbajoria/ssh2-promise) from 0.2.0 to 1.0.2.
- [Release notes](https://github.com/sanketbajoria/ssh2-promise/releases)
- [Commits](https://github.com/sanketbajoria/ssh2-promise/commits)

---
updated-dependencies:
- dependency-name: ssh2-promise
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-01 13:52:48 +00:00
7a63608f54 feat(UI): auto detect system theme as default app theme 2021-10-01 15:48:02 +02:00
7ea7b369ab chore: update dependencies 2021-10-01 15:33:10 +02:00
258fffa958 chore: update README.md 2021-09-30 09:05:40 +02:00
0849c5131f Merge pull request #106 from Fabio286/dependabot/npm_and_yarn/electron-15.0.0
build(deps-dev): bump electron from 14.0.1 to 15.0.0
2021-09-28 09:19:16 +02:00
dependabot[bot]
cce59d0ca8 build(deps-dev): bump electron from 14.0.1 to 15.0.0
Bumps [electron](https://github.com/electron/electron) from 14.0.1 to 15.0.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v14.0.1...v15.0.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-27 19:06:30 +00:00
94a53fec6c chore(release): 0.3.6 2021-09-26 11:39:19 +02:00
13aa47cd44 feat: processes list exportation 2021-09-26 11:36:42 +02:00
85f625daf7 feat: copy cell/row or kill connections on context menu from processes list 2021-09-26 11:19:48 +02:00
7de3bb9346 refactor(core): compatibility with electron/remote 2.x 2021-09-21 11:16:07 +02:00
e43a0ba0b4 Merge pull request #102 from Fabio286/dependabot/npm_and_yarn/electron/remote-2.0.1
build(deps): bump @electron/remote from 1.2.2 to 2.0.1
2021-09-21 10:03:35 +02:00
dependabot[bot]
638cd4bfb7 build(deps): bump @electron/remote from 1.2.2 to 2.0.1
Bumps [@electron/remote](https://github.com/electron/remote) from 1.2.2 to 2.0.1.
- [Release notes](https://github.com/electron/remote/releases)
- [Changelog](https://github.com/electron/remote/blob/main/.releaserc.json)
- [Commits](https://github.com/electron/remote/compare/v1.2.2...v2.0.1)

---
updated-dependencies:
- dependency-name: "@electron/remote"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-20 19:06:45 +00:00
3959333662 feat: workspace query history 2021-09-17 18:32:28 +02:00
abd46aa322 perf(core): better communication of internal exceptions 2021-09-15 15:31:57 +02:00
d4888ad8fb fix: adding a connection default values not change when switching clients, closes #101 2021-09-15 11:08:00 +02:00
6bfc229b77 chore(release): 0.3.5 2021-09-13 15:03:02 +02:00
d31b051f4b chore: update dependencies 2021-09-13 15:02:28 +02:00
95b60df8fc fix(MySQL): connections stuck at startup if 5 or more tabs are restored 2021-09-13 10:03:27 +02:00
ed5189fdc1 chore(release): 0.3.4 2021-09-12 11:59:01 +02:00
265ed66d25 feat: start search when typing with focus on the left bar 2021-09-12 11:55:16 +02:00
09c07acd5c feat: new create trigger function tabs 2021-09-11 10:24:21 +02:00
3c5a69adc9 feat: new create scheduler tabs 2021-09-10 18:23:32 +02:00
0203f69e95 feat: new create function tabs 2021-09-07 18:20:45 +02:00
9a2498862c Merge pull request #97 from Fabio286/dependabot/npm_and_yarn/electron-14.0.0
build(deps-dev): bump electron from 13.3.0 to 14.0.0
2021-09-07 09:07:29 +02:00
dependabot[bot]
3a26d4f509 build(deps-dev): bump electron from 13.3.0 to 14.0.0
Bumps [electron](https://github.com/electron/electron) from 13.3.0 to 14.0.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v13.3.0...v14.0.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-06 19:44:11 +00:00
ce08cbd8b5 Merge pull request #98 from Fabio286/dependabot/npm_and_yarn/mdi/font-6.1.95
build(deps): bump @mdi/font from 5.9.55 to 6.1.95
2021-09-06 21:41:47 +02:00
dependabot[bot]
d91ffcccca build(deps): bump @mdi/font from 5.9.55 to 6.1.95
Bumps [@mdi/font](https://github.com/Templarian/MaterialDesign-Webfont) from 5.9.55 to 6.1.95.
- [Release notes](https://github.com/Templarian/MaterialDesign-Webfont/releases)
- [Commits](https://github.com/Templarian/MaterialDesign-Webfont/compare/v5.9.55...v6.1.95)

---
updated-dependencies:
- dependency-name: "@mdi/font"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-06 19:06:42 +00:00
6115eb9409 refactor: improvements in explorebar events on tables 2021-09-06 18:02:54 +02:00
3fd26a0523 feat: new create routine tabs 2021-09-06 17:29:34 +02:00
e217d5181b feat: new create trigger tabs 2021-09-02 18:08:23 +02:00
dcf368b350 chore: update dependencies 2021-09-01 18:17:11 +02:00
21e3e79ddf fix(UI): context menu of tables cut if close to bottom edge 2021-08-31 19:58:20 +02:00
2918c3cb92 chore(release): 0.3.3 2021-08-22 10:41:48 +02:00
3ad190b18c Merge branch 'master' of https://github.com/Fabio286/antares 2021-08-22 10:37:27 +02:00
d5b2bde2ea fix(UI): no round borders on left of file upload inputs 2021-08-22 10:37:23 +02:00
a42348ef5c chore: deletion of wrong file name case component 2021-08-20 13:42:29 +02:00
8b93c49778 feat: new create view tabs 2021-08-18 17:28:41 +02:00
0842e00098 feat: new table empty state 2021-08-17 18:54:23 +02:00
6cef02bebb Merge pull request #93 from Fabio286/dependabot/npm_and_yarn/marked-3.0.0
build(deps): bump marked from 2.1.3 to 3.0.0
2021-08-17 12:04:22 +02:00
dependabot[bot]
334c7a31d2 build(deps): bump marked from 2.1.3 to 3.0.0
Bumps [marked](https://github.com/markedjs/marked) from 2.1.3 to 3.0.0.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/release.config.js)
- [Commits](https://github.com/markedjs/marked/compare/v2.1.3...v3.0.0)

---
updated-dependencies:
- dependency-name: marked
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-16 19:05:09 +00:00
bc82289d54 perf(UI): visual improvements of tables 2021-08-16 19:44:55 +02:00
c9fa941578 feat: new create table tabs 2021-08-13 16:50:59 +02:00
4048df3c7b fix(MySQL): editing a view causes error for missing database in some conditions 2021-08-12 10:29:13 +02:00
198368605b perf(UI): improved view setting tab 2021-08-12 10:07:31 +02:00
8f0ac26b69 refactor: better component names 2021-08-12 09:54:13 +02:00
b35fc5b78b fix(UI): multiple temp tabs opened switching to tables from other elements 2021-08-11 18:04:14 +02:00
622b519cbb fix: table options not loaded on restored setting tabs at startup 2021-08-11 16:16:58 +02:00
71e2c911ae perf(UI): element options in setting tabs accessible directly 2021-08-10 18:12:13 +02:00
756d49b259 perf(UI): primary app color on selected text backgrouns 2021-08-10 13:14:48 +02:00
14c64c537c chore(release): 0.3.2 2021-08-06 14:44:45 +02:00
97b3563e25 chore: update dependencies 2021-08-06 14:38:25 +02:00
e834fe31ac feat(UI): automatic scroll on selected tab 2021-08-05 13:44:48 +02:00
065de3a0a2 feat(UI): query tab name based on content 2021-08-05 13:30:33 +02:00
1573de5b1f feat(UI): button to clear sidebar search input 2021-08-05 12:47:24 +02:00
04fc1bbee0 feat(UI): automatic scroll to selected tab element in left bar 2021-08-05 12:09:54 +02:00
dea378014d perf: approximate table total updated on table refresh 2021-08-04 15:52:26 +02:00
3abff36136 feat: contextual menu option to duplicate table fields 2021-08-04 09:59:50 +02:00
70354aa828 feat(UI): shortcuts info on empty query tab 2021-08-03 17:59:15 +02:00
372049ae64 perf(UI): loading animation on tables and table context menu improvements 2021-08-03 15:43:13 +02:00
5d271be062 chore: update README.md 2021-08-02 19:26:56 +02:00
07ee1ae828 fix: tab selected when clicking closing cross 2021-07-29 16:45:28 +02:00
cbe0e2980a perf: update italian translation 2021-07-27 17:31:51 +02:00
0fba50c6ec chore: update README.md 2021-07-27 17:18:11 +02:00
420255cdd4 chore(release): 0.3.1 2021-07-27 10:27:02 +02:00
a8a47ed5f7 fix(UI): tabs or explorebar elements selected with mouse wheel or right button 2021-07-23 22:41:53 +02:00
425f0663f9 chore(release): 0.3.0 2021-07-23 17:38:31 +02:00
e106d100b5 refactor: mousedown instead click on tabs and explorebar elements 2021-07-23 15:45:06 +02:00
d7fdf53932 fix: wrong editor height with some conditions 2021-07-23 10:56:41 +02:00
62f7e57d0c fix: manual page input not disabled when only one page is available 2021-07-23 09:01:50 +02:00
14577d14bb fix: sort order of tables is lost switching pages 2021-07-22 18:22:47 +02:00
f5f2a697e8 ci: update snap store config 2021-07-22 17:30:07 +02:00
a0105cf1c3 fix(UI): not disabled buttons during save table setting tabs 2021-07-22 13:13:26 +02:00
77c5d28032 fix: new field default value unknown instead 'noval' 2021-07-22 13:06:54 +02:00
d1d8592f79 build: downgrade to ss2-promise 0.2.0 due build problems 2021-07-22 11:34:21 +02:00
1f828f69a0 Merge pull request #87 from Fabio286/new-tab-system
New tab system
2021-07-22 10:57:43 +02:00
adc5477673 feat: option to restore session on startup 2021-07-22 10:41:06 +02:00
1e543aa6b0 fix: reload twice after element rename 2021-07-21 18:50:22 +02:00
c41e059b0b fix: wrong loaded schema change 2021-07-21 17:56:55 +02:00
e6ef5ffa56 refactor: improved the way how schema is passed to client classes 2021-07-21 14:40:29 +02:00
d67d122270 Merge branch 'master' of https://github.com/Fabio286/antares into new-tab-system 2021-07-21 09:11:06 +02:00
b837e2fc68 Merge pull request #86 from Fabio286/dependabot/npm_and_yarn/ssh2-promise-1.0.0
build(deps): bump ssh2-promise from 0.1.9 to 1.0.0
2021-07-21 09:10:35 +02:00
a73a2f483e feat: option to select schema in query tabs 2021-07-20 19:19:54 +02:00
0a9983d30d feat: new function, procedure and scheduler tabs 2021-07-20 16:59:59 +02:00
58b91ebfe0 feat: new trigger function tabs 2021-07-19 22:38:56 +02:00
e78ca2417e fix(UI): multiple trigger tabs open on single click on explore bar 2021-07-19 11:28:11 +02:00
e1855a262d feat(UI): empty workspace view 2021-07-18 16:10:36 +02:00
6b725b1d40 fix: issues with trigger temp tabs 2021-07-17 16:09:57 +02:00
f6faad98f8 feat: new trigger setting tabs 2021-07-17 13:10:54 +02:00
320aa8ba04 feat: close tabs if element deleted 2021-07-17 10:46:24 +02:00
9f0280b991 Merge branch 'master' of https://github.com/Fabio286/antares into new-tab-system 2021-07-17 10:00:46 +02:00
04fa320820 fix: clear empty indexes and foreign keys on confirm respective modals 2021-07-17 09:59:45 +02:00
f7c3aa883d fix: tab won't open after table or view creation 2021-07-17 09:57:49 +02:00
f7a74df009 feat: new unsaved change reminder 2021-07-16 23:24:55 +02:00
003c02b1fb feat: new view setting tabs 2021-07-16 18:52:18 +02:00
ef21ea7448 feat: rename tabs if element is renamed 2021-07-16 17:27:37 +02:00
525c964c62 fix: enabled copy context on non editable rows 2021-07-16 17:09:02 +02:00
7845e3e501 feat(UI): new table settings tabs 2021-07-15 19:51:18 +02:00
0c29e0d566 perf(UI): improvements in setting bar connections sort 2021-07-15 18:31:44 +02:00
d38097d056 feat(UI): sortable tabs 2021-07-14 20:30:54 +02:00
c87b8dc738 refactor: passing schema from table context options 2021-07-14 18:15:13 +02:00
ed6e7fa72d refactor(UI): improved breadcrumbs and tabs 2021-07-14 16:10:34 +02:00
f0fa7c81b7 fix(UI): table icon in view data tabs 2021-07-14 12:33:26 +02:00
5bb4e496f2 feat(MySQL): improved schema detection for queries 2021-07-14 12:31:37 +02:00
01057332b0 feat(UI): display schema in data tabs 2021-07-14 12:31:12 +02:00
ab382dfbcd feat: new data tabs 2021-07-13 19:23:02 +02:00
88c4cdc8e2 feat(UI): close temp data tabs 2021-07-13 16:53:47 +02:00
15ff211a41 Merge branch 'master' of https://github.com/Fabio286/antares into new-tab-system 2021-07-13 09:19:26 +02:00
5c855a520a fix: solved a vulnerability in table names 2021-07-13 09:17:22 +02:00
7488bc7a17 refactor(core): better way to obtain schema 2021-07-13 09:09:25 +02:00
dependabot[bot]
fd85cf43a2 build(deps): bump ssh2-promise from 0.1.9 to 1.0.0
Bumps [ssh2-promise](https://github.com/sanketbajoria/ssh2-promise) from 0.1.9 to 1.0.0.
- [Release notes](https://github.com/sanketbajoria/ssh2-promise/releases)
- [Commits](https://github.com/sanketbajoria/ssh2-promise/commits/1.0.0)

---
updated-dependencies:
- dependency-name: ssh2-promise
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-12 19:05:27 +00:00
a87079cd17 feat(UI): temporary table data tabs 2021-07-12 19:18:29 +02:00
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
161 changed files with 12664 additions and 4292 deletions

117
.all-contributorsrc Normal file
View File

@@ -0,0 +1,117 @@
{
"projectName": "Antares",
"projectOwner": "Fabio286",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitConvention": "angular",
"contributors": [
{
"login": "Fabio286",
"name": "Fabio Di Stasio",
"avatar_url": "https://avatars.githubusercontent.com/u/31471771?v=4",
"profile": "https://fabiodistasio.it/",
"contributions": [
"code",
"translation",
"doc"
]
},
{
"login": "toriphes",
"name": "Giulio Ganci",
"avatar_url": "https://avatars.githubusercontent.com/u/4192159?v=4",
"profile": "https://www.linkedin.com/in/giulioganci/",
"contributions": [
"code"
]
},
{
"login": "digitalgopnik",
"name": "Christian Ratz",
"avatar_url": "https://avatars.githubusercontent.com/u/2630316?v=4",
"profile": "https://christianratz.de/",
"contributions": [
"code",
"translation"
]
},
{
"login": "reverb6821",
"name": "Giuseppe Gigliotti",
"avatar_url": "https://avatars.githubusercontent.com/u/55198803?v=4",
"profile": "https://reverb6821.github.io/",
"contributions": [
"translation"
]
},
{
"login": "Mohd-PH",
"name": "Mohd-PH",
"avatar_url": "https://avatars.githubusercontent.com/u/9362157?v=4",
"profile": "https://github.com/Mohd-PH",
"contributions": [
"translation"
]
},
{
"login": "hongkfui",
"name": "hongkfui",
"avatar_url": "https://avatars.githubusercontent.com/u/37477191?v=4",
"profile": "https://github.com/hongkfui",
"contributions": [
"translation"
]
},
{
"login": "MrAnyx",
"name": "Robin",
"avatar_url": "https://avatars.githubusercontent.com/u/44176707?v=4",
"profile": "https://github.com/MrAnyx",
"contributions": [
"translation"
]
},
{
"login": "daeleduardo",
"name": "Daniel Eduardo",
"avatar_url": "https://avatars.githubusercontent.com/u/8599078?v=4",
"profile": "https://github.com/daeleduardo",
"contributions": [
"translation"
]
},
{
"login": "datlechin",
"name": "Ngô Quốc Đạt",
"avatar_url": "https://avatars.githubusercontent.com/u/56961917?v=4",
"profile": "https://ngoquocdat.com/",
"contributions": [
"translation"
]
},
{
"login": "IsamuSugi",
"name": "Isamu Sugiura",
"avatar_url": "https://avatars.githubusercontent.com/u/7746658?v=4",
"profile": "https://github.com/IsamuSugi",
"contributions": [
"translation"
]
},
{
"login": "Occhioverde",
"name": "Riccardo Sacchetto",
"avatar_url": "https://avatars.githubusercontent.com/u/18429412?v=4",
"profile": "http://rsacchetto.nexxontech.it/",
"contributions": [
"platform"
]
}
],
"contributorsPerLine": 7
}

View File

@@ -1,4 +1,4 @@
/node_modules
/assets/vendor
/out
/dist
node_modules
assets
out
dist

View File

@@ -45,7 +45,9 @@
"no-console": "off",
"no-undef": "off",
"vue/no-side-effects-in-computed-properties": "off",
"vue/multi-word-component-names": "off",
"vue/require-default-prop": "off",
"vue/comment-directive": "off",
"vue/no-v-html": "off",
"vue/html-indent": [
"error",
@@ -60,10 +62,11 @@
"vue/max-attributes-per-line": [
"error",
{
"singleline": 2,
"singleline": {
"max": 2
},
"multiline": {
"max": 1,
"allowFirstLine": false
"max": 1
}
}
]

View File

@@ -17,7 +17,7 @@ jobs:
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1

View File

@@ -17,7 +17,7 @@ jobs:
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1

View File

@@ -17,7 +17,7 @@ jobs:
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1

9
.gitignore vendored
View File

@@ -1,9 +1,8 @@
.DS_Store
dist/
node_modules/
dist
build
node_modules
thumbs.db
.idea/
.vscode
TODO.md
NOTES.md
*.txt
package-lock.json

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v16.13.0

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

29
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
"version": "0.2.0",
"configurations": [
{
"cwd": "${workspaceFolder}",
"name": "Electron: Main",
"port": 9222,
"protocol": "inspector",
"request": "attach",
"sourceMaps": true,
"type": "node",
"timeout": 1000000
},
{
"name": "Electron: Renderer",
"port": 9223,
"request": "attach",
"sourceMaps": true,
"type": "chrome",
"webRoot": "${workspaceFolder}"
}
],
"compounds": [
{
"name": "Electron: All",
"configurations": ["Electron: Main", "Electron: Renderer"]
}
]
}

9
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"conventionalCommits.scopes": [
"UI",
"core",
"MySQL",
"PostgreSQL"
],
"svg.preview.background": "transparent"
}

View File

@@ -2,6 +2,322 @@
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.3.9](https://github.com/Fabio286/antares/compare/v0.3.8...v0.3.9) (2021-11-14)
### Features
* added macos basic shortcusts and menu ([430490a](https://github.com/Fabio286/antares/commit/430490ad93f3148962ced1f13a5330c79cd86b3b))
* **MySQL:** enable/disable schedulers from contextual menu ([5ca3a22](https://github.com/Fabio286/antares/commit/5ca3a22dc538b27a4bf6402f1288c4b9f5bc5a90))
* **MySQL:** scheduler status indicator in explore bar ([5c66824](https://github.com/Fabio286/antares/commit/5c668249cf102cd9d601f9f7b4943c7155775217))
* **PostgreSQL:** enable/disable triggers from contextual menu ([534659f](https://github.com/Fabio286/antares/commit/534659f9aee12eb5ac477f91bfe5d764387dc17e))
* schema size in explore bar ([fd25f88](https://github.com/Fabio286/antares/commit/fd25f881f95779709156cbad93a41d6b391f1a45))
* **UI:** double click on the title bar will toggle window fullscreen size ([a35566f](https://github.com/Fabio286/antares/commit/a35566f273322602abe434b8bd30817ba8885900))
* **UI:** improved topbar look&feel on MacOS ([7657d05](https://github.com/Fabio286/antares/commit/7657d05edfbeaed6a14eb337fc562da5126e6ba0))
### Bug Fixes
* copy&paste and basic usability on macOS ([1ddf8f0](https://github.com/Fabio286/antares/commit/1ddf8f0dbe22f94d6bffddf70636706d2d142ecf))
* **PostgreSQL:** bigint fetched as string instead of number, closes [#134](https://github.com/Fabio286/antares/issues/134) ([39b9a59](https://github.com/Fabio286/antares/commit/39b9a59143b457a96f0711a3b8588c92dd80e28d))
* row selection problems after a deletion fail, closes [#128](https://github.com/Fabio286/antares/issues/128) ([89fdd21](https://github.com/Fabio286/antares/commit/89fdd210ca48fc9ae399b195ea796c8523619627))
* temporary solution on MacOS for unsigned app updates ([c00fd13](https://github.com/Fabio286/antares/commit/c00fd1381f451ba7aace7047b28b904ddcaf18f0))
### Improvements
* **UI:** improved function and routine parameters modals ([d19f475](https://github.com/Fabio286/antares/commit/d19f475fc28c0367ada569cb634769fa618b48b4))
### [0.3.8](https://github.com/Fabio286/antares/compare/v0.3.7...v0.3.8) (2021-10-23)
### Features
* **UI:** CTRL+A to select all result rows ([4069ade](https://github.com/Fabio286/antares/commit/4069ade36df43e58f198dd1305778b5811824315))
* **UI:** ctrl|cmd+t, ctrl|cmd+w shortcut to open/close workspace tabs ([9046b85](https://github.com/Fabio286/antares/commit/9046b858b1e4608af4c01bc4d69e1a49d4009c07))
* **UI:** hide filter bar if there are no more rows in it ([9178805](https://github.com/Fabio286/antares/commit/91788054e6302e83cb4a7501ad6c3f72809cb3bb))
* **UI:** multi column table filters ([0e15c39](https://github.com/Fabio286/antares/commit/0e15c39797fe34f7a649f85ee62204682d45c98a))
### Bug Fixes
* **PostgreSQL:** issue with uppercase characters in table field names ([aef17be](https://github.com/Fabio286/antares/commit/aef17be36cfcf3a6325a954e80f973623e250405))
* query failure when a filter with a numeric value is used ([69cd083](https://github.com/Fabio286/antares/commit/69cd083054cae50d64475b9f1f5d7ebd39093e39))
* regression during resize results table on filters change ([7dc33c7](https://github.com/Fabio286/antares/commit/7dc33c78aa4152264cc6833437be9af9b8621867))
### Improvements
* **UI:** resize results table on filters change ([f9ee7d0](https://github.com/Fabio286/antares/commit/f9ee7d0450a1386800223d7b96849e06ae02aece))
### [0.3.7](https://github.com/Fabio286/antares/compare/v0.3.6...v0.3.7) (2021-10-08)
### Features
* support to SSH private keys with passphrase, closes [#118](https://github.com/Fabio286/antares/issues/118) ([9407a29](https://github.com/Fabio286/antares/commit/9407a29922812ab6aa3cf67569ba2f509433657c))
* **UI:** auto detect system theme as default app theme ([7a63608](https://github.com/Fabio286/antares/commit/7a63608f54e387d45e655855666041f5602b54b1))
### Bug Fixes
* closing ask credential modal during a connection doesn't stops loading, closes [#114](https://github.com/Fabio286/antares/issues/114) ([26446fb](https://github.com/Fabio286/antares/commit/26446fb7ed04216283736072d442786350252dbb))
* **PostgreSQL:** issue with uppercase characters in table and field names, closes [#116](https://github.com/Fabio286/antares/issues/116) ([2fcd080](https://github.com/Fabio286/antares/commit/2fcd080bd47367a21590ea5a754410a975959bdd))
* **UI:** window reload pressing enter in schema creation modal, closes [#113](https://github.com/Fabio286/antares/issues/113) ([db1641b](https://github.com/Fabio286/antares/commit/db1641b74fcd218f1f1d24163cba70b024cc6bd7))
### [0.3.6](https://github.com/Fabio286/antares/compare/v0.3.5...v0.3.6) (2021-09-26)
### Features
* copy cell/row or kill connections on context menu from processes list ([85f625d](https://github.com/Fabio286/antares/commit/85f625daf7026815dac6223a29c5a6479830edbb))
* processes list exportation ([13aa47c](https://github.com/Fabio286/antares/commit/13aa47cd4441aa47c93038dbd91d6a0e54f6a60c))
* workspace query history ([3959333](https://github.com/Fabio286/antares/commit/39593336626e6d9f3d3b65d2a4081388900e37d6))
### Bug Fixes
* adding a connection default values not change when switching clients, closes [#101](https://github.com/Fabio286/antares/issues/101) ([d4888ad](https://github.com/Fabio286/antares/commit/d4888ad8fba3c8e8ec2d6b6d9a78bb212d83eeed))
### Improvements
* **core:** better communication of internal exceptions ([abd46aa](https://github.com/Fabio286/antares/commit/abd46aa32256f822e52eaac2fc698da378b8163f))
### [0.3.5](https://github.com/Fabio286/antares/compare/v0.3.4...v0.3.5) (2021-09-13)
### Bug Fixes
* **MySQL:** connections stuck at startup if 5 or more tabs are restored ([95b60df](https://github.com/Fabio286/antares/commit/95b60df8fc634b96a4c2c5c48dc6b10848888978))
### [0.3.4](https://github.com/Fabio286/antares/compare/v0.3.3...v0.3.4) (2021-09-12)
### Features
* new create function tabs ([0203f69](https://github.com/Fabio286/antares/commit/0203f69e95093d25a7ef3e66df1c70f76edcedf2))
* new create routine tabs ([3fd26a0](https://github.com/Fabio286/antares/commit/3fd26a05238368ae375197c79d42162a5910bb07))
* new create scheduler tabs ([3c5a69a](https://github.com/Fabio286/antares/commit/3c5a69adc9cecdc4b8f46097676005f2a60f06cf))
* new create trigger function tabs ([09c07ac](https://github.com/Fabio286/antares/commit/09c07acd5c2a6ed2b75640dbb83e782ed432bc30))
* new create trigger tabs ([e217d51](https://github.com/Fabio286/antares/commit/e217d5181b37ec6304151120b4a2aba9455c6a84))
* start search when typing with focus on the left bar ([265ed66](https://github.com/Fabio286/antares/commit/265ed66d25d35be99ed0a6b1668dab9f246ed71e))
### Bug Fixes
* **UI:** context menu of tables cut if close to bottom edge ([21e3e79](https://github.com/Fabio286/antares/commit/21e3e79ddf9e292bcfc5881b8fa76a1dc58b207c))
### [0.3.3](https://github.com/Fabio286/antares/compare/v0.3.2...v0.3.3) (2021-08-22)
### Features
* new create table tabs ([c9fa941](https://github.com/Fabio286/antares/commit/c9fa9415787c3953043db5876a99b3664c69c071))
* new create view tabs ([8b93c49](https://github.com/Fabio286/antares/commit/8b93c497784ea431f9747c5afb53f6ef075ea9d6))
* new table empty state ([0842e00](https://github.com/Fabio286/antares/commit/0842e00098ba420412937aa52276ee33bda53693))
### Bug Fixes
* **MySQL:** editing a view causes error for missing database in some conditions ([4048df3](https://github.com/Fabio286/antares/commit/4048df3c7bc2d42a60f7a57c9a4b8b5b445fcd43))
* table options not loaded on restored setting tabs at startup ([622b519](https://github.com/Fabio286/antares/commit/622b519cbb5fbe4e38a4baffb8eab169b21eed21))
* **UI:** multiple temp tabs opened switching to tables from other elements ([b35fc5b](https://github.com/Fabio286/antares/commit/b35fc5b78bdbeff1422ef088441b17c8b0df663c))
* **UI:** no round borders on left of file upload inputs ([d5b2bde](https://github.com/Fabio286/antares/commit/d5b2bde2eaf8ff3e14f49cc26acdcb201b4cb15c))
### Improvements
* **UI:** element options in setting tabs accessible directly ([71e2c91](https://github.com/Fabio286/antares/commit/71e2c911ae23e86543b2af1fa885981ff271777d))
* **UI:** improved view setting tab ([1983686](https://github.com/Fabio286/antares/commit/198368605b084bd58fd6f7ca0b19895ba23a45e6))
* **UI:** primary app color on selected text backgrouns ([756d49b](https://github.com/Fabio286/antares/commit/756d49b2596dea58376c6afa8e0bad0cd62b146c))
* **UI:** visual improvements of tables ([bc82289](https://github.com/Fabio286/antares/commit/bc82289d54550a93300fe66d7a660aa70db2fd23))
### [0.3.2](https://github.com/Fabio286/antares/compare/v0.3.1...v0.3.2) (2021-08-06)
### Features
* contextual menu option to duplicate table fields ([3abff36](https://github.com/Fabio286/antares/commit/3abff3613618ddc86e1d6c898f83bcb360e8e5d9))
* **UI:** automatic scroll on selected tab ([e834fe3](https://github.com/Fabio286/antares/commit/e834fe31ac8b73f249e3ad8654e97da834b28cfe))
* **UI:** automatic scroll to selected tab element in left bar ([04fc1bb](https://github.com/Fabio286/antares/commit/04fc1bbee0cdc3b02288ff5d3b2c402633cae1f1))
* **UI:** button to clear sidebar search input ([1573de5](https://github.com/Fabio286/antares/commit/1573de5b1f545328523aaaaac541f5a8046617be))
* **UI:** query tab name based on content ([065de3a](https://github.com/Fabio286/antares/commit/065de3a0a28a9e6667df4e41ac1c5fa5561d2171))
* **UI:** shortcuts info on empty query tab ([70354aa](https://github.com/Fabio286/antares/commit/70354aa828121004f70e91d65cb9f0a4811dce57))
### Bug Fixes
* tab selected when clicking closing cross ([07ee1ae](https://github.com/Fabio286/antares/commit/07ee1ae828bb5f9971a263f2e39e73c760fed50a))
### Improvements
* approximate table total updated on table refresh ([dea3780](https://github.com/Fabio286/antares/commit/dea378014dd45bf90f51a599b4a801e0bc22059f))
* **UI:** loading animation on tables and table context menu improvements ([372049a](https://github.com/Fabio286/antares/commit/372049ae64232e8e61a974edc8be5b319c5c0811))
* update italian translation ([cbe0e29](https://github.com/Fabio286/antares/commit/cbe0e2980a9d4562941aec37a57cc936a46f41d8))
### [0.3.1](https://github.com/Fabio286/antares/compare/v0.3.0...v0.3.1) (2021-07-27)
### Bug Fixes
* **UI:** tabs or explorebar elements selected with mouse wheel or right button ([a8a47ed](https://github.com/Fabio286/antares/commit/a8a47ed5f7d5d8cbbdd6c33ef8307d9dce5b193b))
## [0.3.0](https://github.com/Fabio286/antares/compare/v0.2.1...v0.3.0) (2021-07-23)
### Features
* close tabs if element deleted ([320aa8b](https://github.com/Fabio286/antares/commit/320aa8ba04d464f628b938b812165cd1ec786492))
* new data tabs ([ab382df](https://github.com/Fabio286/antares/commit/ab382dfbcd89f8ab38a8cbdb4d87e1705618fd0d))
* new function, procedure and scheduler tabs ([0a9983d](https://github.com/Fabio286/antares/commit/0a9983d30d6aeb9aee1c68b31a31f8c605deea45))
* new trigger function tabs ([58b91eb](https://github.com/Fabio286/antares/commit/58b91ebfe0ab200d76d4cfd442b9f2970ae97384))
* new trigger setting tabs ([f6faad9](https://github.com/Fabio286/antares/commit/f6faad98f88222500dfb7265b74c1be914b894cd))
* new unsaved change reminder ([f7a74df](https://github.com/Fabio286/antares/commit/f7a74df0097867a82a2a2d8b3c278a81897d7898))
* new view setting tabs ([003c02b](https://github.com/Fabio286/antares/commit/003c02b1fbe5aba7f53f3faa5610b0c2f7706793))
* option to restore session on startup ([adc5477](https://github.com/Fabio286/antares/commit/adc5477673603cd63fabf77a53db5397e3774e0e))
* option to select schema in query tabs ([a73a2f4](https://github.com/Fabio286/antares/commit/a73a2f483ef6927def4ba635e306277c34ae4b53))
* **UI:** empty workspace view ([e1855a2](https://github.com/Fabio286/antares/commit/e1855a262dc24363fc143a38a70154938308bd71))
* rename tabs if element is renamed ([ef21ea7](https://github.com/Fabio286/antares/commit/ef21ea74481839ba67ca79d40d5f47e1c12aebc0))
* **MySQL:** improved schema detection for queries ([5bb4e49](https://github.com/Fabio286/antares/commit/5bb4e496f289f3ae8a46f24d1a2fbb896d9da86e))
* **UI:** close temp data tabs ([88c4cdc](https://github.com/Fabio286/antares/commit/88c4cdc8e2a60ffbe26b0f831184c3a6dd9a1637))
* **UI:** display schema in data tabs ([0105733](https://github.com/Fabio286/antares/commit/01057332b090997c107bf395bb1fc3b9195e8218))
* **UI:** new table settings tabs ([7845e3e](https://github.com/Fabio286/antares/commit/7845e3e501bb7f890d07b42d4f3eb5f9f4bc8586))
* **UI:** sortable tabs ([d38097d](https://github.com/Fabio286/antares/commit/d38097d0567020b5265b5a0b347f5e1f38e0b1d4))
* **UI:** temporary table data tabs ([a87079c](https://github.com/Fabio286/antares/commit/a87079cd179033cebb6fd228ad7f1b991f3b6c46))
### Bug Fixes
* clear empty indexes and foreign keys on confirm respective modals ([04fa320](https://github.com/Fabio286/antares/commit/04fa320820df1f70a4ef05e4a6e4cbcd4081d047))
* enabled copy context on non editable rows ([525c964](https://github.com/Fabio286/antares/commit/525c964c623e7574f6abe732a2b315b8805d5eee))
* issues with trigger temp tabs ([6b725b1](https://github.com/Fabio286/antares/commit/6b725b1d40753ad89bfad6f1df57c6fb737e5262))
* manual page input not disabled when only one page is available ([62f7e57](https://github.com/Fabio286/antares/commit/62f7e57d0ccf6041b5469bec6a1864aa5b045609))
* new field default value unknown instead 'noval' ([77c5d28](https://github.com/Fabio286/antares/commit/77c5d280325792e20befc845f3a6834837131e39))
* reload twice after element rename ([1e543aa](https://github.com/Fabio286/antares/commit/1e543aa6b0b63e9134bf544682a26bd98573b794))
* solved a vulnerability in table names ([5c855a5](https://github.com/Fabio286/antares/commit/5c855a520a6bc66cc00b0b8afc6d2c03c75c0fab))
* sort order of tables is lost switching pages ([14577d1](https://github.com/Fabio286/antares/commit/14577d14bb337898a1fd0fdee1c3760812b9d21f))
* wrong editor height with some conditions ([d7fdf53](https://github.com/Fabio286/antares/commit/d7fdf53932a4d0609a88641c40d451c71b12c242))
* **UI:** multiple trigger tabs open on single click on explore bar ([e78ca24](https://github.com/Fabio286/antares/commit/e78ca2417e8f996fa0507c5a5586a75393dfe8ee))
* **UI:** not disabled buttons during save table setting tabs ([a0105cf](https://github.com/Fabio286/antares/commit/a0105cf1c37144ea27b212029d39275918f4f95e))
* tab won't open after table or view creation ([f7c3aa8](https://github.com/Fabio286/antares/commit/f7c3aa883dfcad0a28222c53cf22ab49d381559c))
* wrong loaded schema change ([c41e059](https://github.com/Fabio286/antares/commit/c41e059b0ba6d8c6d545c3b86a21a9a2abb6f537))
* **UI:** table icon in view data tabs ([f0fa7c8](https://github.com/Fabio286/antares/commit/f0fa7c81b7aa2a05833ce0b243afed39db98d66b))
### Improvements
* **UI:** improvements in setting bar connections sort ([0c29e0d](https://github.com/Fabio286/antares/commit/0c29e0d566c792ffd1b2b7045124e170b9c51985))
### [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)

107
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,107 @@
# Contributors Guide
Antares SQL is an application based on [Electron.js](https://www.electronjs.org/) that uses [Vue.js](https://vuejs.org/) and [Spectre.css](https://picturepan2.github.io/spectre/) as frontend frameworks.
For the build process it takes advantage of [electron-builder](https://www.electron.build/).
This application uses [Vuex](https://vuex.vuejs.org/) as application state manager and [electron-store](https://github.com/sindresorhus/electron-store) to save the various settings on disc.
This guide aims to provide useful information and guidelines to everyone wants to contribute with this open-source project.
For every other question related to this project please [contact me](https://github.com/Fabio286).
## Project Structure
The main files of the application are located inside `src` folder and are groupped in three subfolders.
### `common`
This folder contains small libraries, classes and objects. The purpose of `common` folder is to group together utilities used by **renderer** and **main** processes.
Noteworthy is the `customizations` folder that contains clients related customizations. Those settings are merged with `default.js` that lists every option.
Client related customizations are stored on Vuex and can be accessed by `customizations` property of current workspace object, or importing `common/customizations`.
An use case of customizations object can be the following:
```js
computed: {
defaultEngine () {
if (this.workspace.customizations.engines)
return this.workspace.engines.find(engine => engine.isDefault).name;
return '';
}
}
```
In this case the computed property `defaultEngine` returns the default engine for MySQL client, or an empty string with PostgreSQL that doesn't have engines.
Customization properties are also useful **if some features are ready for one client but not others**.
### `main`
Inside this folder are located all files required by main process.
`ipc-handlers` subfolder includes all IPC handlers for events sent from renderer process.
`libs` subfolder includes classes related to clients and **query and connection logics**.
**Everything above client's class level should be "client agnostic"** with a neutral and uniformed api interface
### `renderer`
In this folder is located the structure of Vue frontend application.
## Build
The command to build Antares SQL locally is `npm run build:local`.
`build` command (without `:local`) is used exclusively by the GitHub Action.
## Conventions
### Electron
- **kebab-case** for IPC event names.
### Vue
- **PascalCase** for file names (with .vue extension) and including components inside others (`<MyComponent/>`).
- "**Base**" prefix for [base component names](https://vuejs.org/v2/style-guide/#Base-component-names-strongly-recommended).
- "**The**" prefix for [single-instance component names](https://vuejs.org/v2/style-guide/#Single-instance-component-names-strongly-recommended).
- [Tightly coupled component names](https://vuejs.org/v2/style-guide/#Tightly-coupled-component-names-strongly-recommended).
- [Order of words in component names](https://vuejs.org/v2/style-guide/#Order-of-words-in-component-names-strongly-recommended).
- **kebab-case** in templates for property and event names.
### Vuex
- **snake_case** for state names.
- **camelCase** for getter and action names.
- **SNAKE_CASE (all caps)** for mutation names.
### Code Style
The project includes [ESlint](https://eslint.org/) and [StyleLint](https://stylelint.io/) config files with style rules. I recommend to set the lint on-save option in your code editor.
Alternatively you can launch following commands to lint the project.
Check if all the style rules have been followed:
```console
npm run lint
```
Apply style rules globally if possible:
```console
npm run lint:fix
```
### Other recommendations
Please, use if possible **template literals** to compose strings and **avoid unnecessary dependencies**.
### Commits
The commit style adopted for this project is [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
Basicly it's important to have **single scoped commits with a prefix** that follows this style because Antares SQL uses [standard-version](https://github.com/conventional-changelog/standard-version) to generate new releases and [CHANGELOG.md](https://github.com/Fabio286/antares/blob/master/CHANGELOG.md) file to track all notable changes.
For Visual Studio Code users may be useful [Conventional Commits](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits) extension.
## Debug
**Debug mode**:
```console
npm run debug
```
After running the debug mode Antares will listen on port 9222 (main process) for a debugger.
On **Visual Studio Code** just launch "*Electron: Main*" configurations after running Antares in debug mode.

119
README.md
View File

@@ -1,72 +1,87 @@
<!-- markdownlint-disable -->
<p align="center">
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/gh-logo.png">
</p>
<!-- markdownlint-restore -->
# Antares SQL Client
![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)
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) ![GitHub](https://img.shields.io/github/license/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) [![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.
Our 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?
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.
## 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)
🚀 **[Other Downloads](https://github.com/Fabio286/antares/releases/latest)**
## How to contribute
- [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
## Current main features
## Current key features
- Multiple database connections at same time.
- Database management (add/edit/delete).
- Full tables management, including indexes and foreign keys.
- Views, triggers, stored routines, functions and schedulers management (add/edit/delete).
- Fake table data filler.
- Run queries on multiple tabs.
- A modern and friendly tab system; keep open every kind of tab you need in your workspace.
- Fake table data filler to generate tons of data for test purpose.
- Query suggestions and auto complete.
- Query history: search through the last 1000 queries.
- SSH tunnel support.
- Dark and light theme.
- Editor themes.
- Scratchpad.
- Multi language.
- Secure password storage.
## Philosophy
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, nested tabs or submenu; productivity comes first.
## Installation
Based on your operating system you can have one or more distribution formats to choose based on your preferences.
Since Antares SQL is a free software we haven't a budget to spend in annual licenses or certificates. This can result that on some platforms you need some additional passages to install this app.
### Linux
On Linux you can simply download and run `.AppImage` distributions, install from Snap Store or from AUR.
### Windows
On Windows you can choose between Microsoft Store and download `.exe` distribution. The latter lacks of a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt.
### MacOS
On macOS you can run `.dmg` distribution following [this guide](https://support.apple.com/guide/mac-help/mh40616/mac) to install apps from unknown developers.
## 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 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)**
## Coming soon
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.
- More context menu shortcuts.
- More keyboard shortcuts.
- Query logs console.
- Import/export and migration.
- Apple Silicon distribution
## Currently supported
### Databases
- [x] MySQL/MariaDB
- [x] PostgreSQL (partially, work in progress)
- [x] PostgreSQL
- [ ] SQLite
- [ ] MSSQL
- [ ] OracleDB
@@ -78,7 +93,7 @@ This is a roadmap with major features will come in near future.
- [x] Windows
- [x] Linux
- [x] MacOS (not tested due lack of hardware)
- [x] MacOS
#### • ARM
@@ -86,14 +101,40 @@ This is a roadmap with major features will come in near future.
- [x] Linux
- [ ] MacOS
## Translations
## How to contribute
**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)]
- 🌍 [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)
## Reviews
## Contributors ✨
<a target="_blank" href="https://www.softx64.com/windows/antares-sql-client.html" title="Antares SQL Client review"><img src="https://www.softx64.com/softx64-review.png" alt="Antares SQL Client review" /></a>
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://fabiodistasio.it/"><img src="https://avatars.githubusercontent.com/u/31471771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fabio Di Stasio</b></sub></a><br /><a href="https://github.com/Fabio286/Antares/commits?author=Fabio286" title="Code">💻</a> <a href="#translation-Fabio286" title="Translation">🌍</a> <a href="https://github.com/Fabio286/Antares/commits?author=Fabio286" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/giulioganci/"><img src="https://avatars.githubusercontent.com/u/4192159?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giulio Ganci</b></sub></a><br /><a href="https://github.com/Fabio286/Antares/commits?author=toriphes" title="Code">💻</a></td>
<td align="center"><a href="https://christianratz.de/"><img src="https://avatars.githubusercontent.com/u/2630316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christian Ratz</b></sub></a><br /><a href="https://github.com/Fabio286/Antares/commits?author=digitalgopnik" title="Code">💻</a> <a href="#translation-digitalgopnik" title="Translation">🌍</a></td>
<td align="center"><a href="https://reverb6821.github.io/"><img src="https://avatars.githubusercontent.com/u/55198803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giuseppe Gigliotti</b></sub></a><br /><a href="#translation-reverb6821" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Mohd-PH"><img src="https://avatars.githubusercontent.com/u/9362157?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mohd-PH</b></sub></a><br /><a href="#translation-Mohd-PH" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/hongkfui"><img src="https://avatars.githubusercontent.com/u/37477191?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hongkfui</b></sub></a><br /><a href="#translation-hongkfui" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/MrAnyx"><img src="https://avatars.githubusercontent.com/u/44176707?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Robin</b></sub></a><br /><a href="#translation-MrAnyx" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/daeleduardo"><img src="https://avatars.githubusercontent.com/u/8599078?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Eduardo</b></sub></a><br /><a href="#translation-daeleduardo" title="Translation">🌍</a></td>
<td align="center"><a href="https://ngoquocdat.com/"><img src="https://avatars.githubusercontent.com/u/56961917?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ngô Quốc Đạt</b></sub></a><br /><a href="#translation-datlechin" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/IsamuSugi"><img src="https://avatars.githubusercontent.com/u/7746658?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Isamu Sugiura</b></sub></a><br /><a href="#translation-IsamuSugi" title="Translation">🌍</a></td>
<td align="center"><a href="http://rsacchetto.nexxontech.it/"><img src="https://avatars.githubusercontent.com/u/18429412?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Riccardo Sacchetto</b></sub></a><br /><a href="#platform-Occhioverde" title="Packaging/porting to new platform">📦</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 50 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: 43 KiB

After

Width:  |  Height:  |  Size: 304 KiB

View File

@@ -1,5 +0,0 @@
{
"include": [
"./src/renderer/**/*"
]
}

View File

@@ -1,27 +1,45 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.1.9",
"description": "A cross-platform easy to use SQL client.",
"version": "0.3.9",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
"scripts": {
"dev": "cross-env NODE_ENV=development electron-webpack dev",
"compile": "electron-webpack",
"debug": "npm run rebuild:electron && npm run debug-runner",
"debug-runner": "node scripts/devRunner.js --remote-debug",
"compile": "npm run compile:main && npm run compile:renderer",
"compile:main": "webpack --mode=production --config webpack.main.config.js",
"compile:renderer": "webpack --mode=production --config webpack.renderer.config.js",
"build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder",
"build:appx": "npm run build:local -- --win appx",
"rebuild:electron": "npm run postinstall && electron-rebuild",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"postinstall": "electron-builder install-app-deps",
"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",
"postinstall": "electron-builder install-app-deps"
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate"
},
"author": "Fabio Di Stasio <fabio286@gmail.com>",
"main": "./dist/main.js",
"build": {
"appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"asar": true,
"directories": {
"output": "build",
"buildResources": "assets"
},
"asarUnpack": "**\\*.{node,dll}",
"files": [
"dist/**/*",
"node_modules",
"package.json"
],
"win": {
"target": [
"nsis",
@@ -61,7 +79,8 @@
"artifactName": "${productName}-${version}-portable.exe"
},
"appx": {
"displayName": "Antares SQL Client",
"displayName": "Antares SQL",
"backgroundColor": "transparent",
"identityName": "62514FabioDiStasio.AntaresSQLClient",
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
"applicationId": "FabioDiStasio.AntaresSQLClient"
@@ -81,54 +100,65 @@
]
}
},
"electronWebpack": {
"renderer": {
"webpackConfig": "webpack.config.js"
}
},
"dependencies": {
"@electron/remote": "^1.1.0",
"@mdi/font": "^5.9.55",
"ace-builds": "^1.4.12",
"electron-log": "^4.3.5",
"electron-store": "^8.0.0",
"@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95",
"ace-builds": "^1.4.13",
"electron-log": "^4.4.1",
"electron-store": "^8.0.1",
"electron-updater": "^4.3.9",
"faker": "^5.5.3",
"marked": "^2.0.2",
"marked": "^4.0.0",
"moment": "^2.29.1",
"mysql2": "^2.2.5",
"pg": "^8.5.1",
"mysql2": "^2.3.2",
"pg": "^8.7.1",
"pgsql-ast-parser": "^7.2.1",
"source-map-support": "^0.5.16",
"source-map-support": "^0.5.20",
"spectre.css": "^0.5.9",
"sql-formatter": "^4.0.2",
"v-mask": "^2.2.4",
"vue-i18n": "^8.24.4",
"ssh2-promise": "^1.0.2",
"v-mask": "^2.3.0",
"vue-i18n": "^8.26.5",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/eslint-parser": "^7.14.3",
"@babel/eslint-parser": "^7.15.7",
"@babel/preset-env": "^7.15.8",
"all-contributors-cli": "^6.20.0",
"babel-loader": "^8.2.3",
"chalk": "^4.1.2",
"clean-webpack-plugin": "^4.0.0",
"cross-env": "^7.0.2",
"electron": "^12.0.9",
"electron-builder": "22.10.5",
"css-loader": "^6.5.0",
"electron": "^15.3.0",
"electron-builder": "^22.13.1",
"electron-devtools-installer": "^3.2.0",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.26.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"electron-rebuild": "^3.2.3",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.9.0",
"sass": "^1.32.13",
"sass-loader": "^10.2.0",
"standard-version": "^9.3.0",
"eslint-plugin-vue": "^8.0.3",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.3",
"node-loader": "^2.0.0",
"progress-webpack-plugin": "^1.0.12",
"sass": "^1.42.1",
"sass-loader": "^12.3.0",
"standard-version": "^9.3.1",
"style-loader": "^3.3.1",
"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",
"webpack": "^4.46.0"
"stylelint-scss": "^3.21.0",
"tree-kill": "^1.2.2",
"vue": "^2.6.14",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.60.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.4.0"
}
}

131
scripts/devRunner.js Normal file
View File

@@ -0,0 +1,131 @@
process.env.NODE_ENV = 'development';
// process.env.ELECTRON_ENABLE_LOGGING = true
const chalk = require('chalk');
const electron = require('electron');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const kill = require('tree-kill');
const path = require('path');
const { spawn } = require('child_process');
const mainConfig = require('../webpack.main.config');
const rendererConfig = require('../webpack.renderer.config');
// const workersConfig = require('../webpack.workers.config');
let electronProcess = null;
let manualRestart = null;
const remoteDebugging = process.argv.includes('--remote-debug');
if (remoteDebugging) {
// disable dvtools open in electron
process.env.RENDERER_REMOTE_DEBUGGING = true;
}
async function killElectron (pid) {
return new Promise((resolve, reject) => {
if (pid) {
kill(pid, 'SIGKILL', err => {
if (err) reject(err);
resolve();
});
}
else
resolve();
});
}
async function restartElectron () {
console.log(chalk.gray('\nStarting electron...'));
const { pid } = electronProcess || {};
await killElectron(pid);
electronProcess = spawn(electron, [
path.join(__dirname, '../dist/main.js'),
// '--enable-logging', // Enable to show logs from all electron processes
remoteDebugging ? '--inspect=9222' : '',
remoteDebugging ? '--remote-debugging-port=9223' : ''
]);
electronProcess.stdout.on('data', data => {
console.log(chalk.white(data.toString()));
});
electronProcess.stderr.on('data', data => {
console.error(chalk.red(data.toString()));
});
electronProcess.on('exit', (code, signal) => {
if (!manualRestart) process.exit(0);
});
}
function startMain () {
const webpackSetup = webpack(mainConfig);
webpackSetup.compilers.forEach((compiler) => {
const { name } = compiler;
switch (name) {
case 'workers':
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(chalk.gray(`\nCompiled ${name} script!`));
console.log(
chalk.gray(`\nWatching file changes for ${name} script...`)
);
});
break;
case 'main':
default:
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(chalk.gray(`\nCompiled ${name} script!`));
manualRestart = true;
await restartElectron();
setTimeout(() => {
manualRestart = false;
}, 2500);
console.log(
chalk.gray(`\nWatching file changes for ${name} script...`)
);
});
break;
}
});
webpackSetup.watch({ aggregateTimeout: 500 }, err => {
if (err) console.error(chalk.red(err));
});
}
function startRenderer (callback) {
const compiler = webpack(rendererConfig);
const { name } = compiler;
compiler.hooks.afterEmit.tap('afterEmit', () => {
console.log(chalk.gray(`\nCompiled ${name} script!`));
console.log(chalk.gray(`\nWatching file changes for ${name} script...`));
});
const server = new WebpackDevServer(compiler, {
hot: true,
port: 9080,
client: {
overlay: true,
logging: 'warn'
}
});
server.startCallback(err => {
if (err) console.error(chalk.red(err));
callback();
});
}
startRenderer(startMain);

View File

@@ -4,9 +4,7 @@ summary: Open source SQL client made to be simple and complete.
description: |
Antares is an SQL client that aims to become an useful and complete tool, especially for developers.
The target is to support as many databases as possible, and all major operating systems, including the ARM versions.
At the moment this application is an alpha and supports only MySQL and x86 architecture.
Most of its current features might be enough for MySQL management, so give it a chance and send us your feedback, we would really appreciate it.
base: core18
base: core20
grade: stable
confinement: strict
@@ -60,6 +58,7 @@ parts:
- fcitx-frontend-gtk3
- libappindicator3-1
- libasound2
- libcurl4
- libgconf-2-4
- libgtk-3-0
- libnotify4
@@ -71,13 +70,59 @@ parts:
- libsecret-1-0
- libxtst6
- libxkbfile1
- gcc-10-base
- libapparmor1
- libblkid1
- libbsd0
- libcom-err2
- libcrypt1
- libdb5.3
- libdbus-1-3
- libexpat1
- libffi7
- libgcc-s1
- libgcrypt20
- libglib2.0-0
- libgmp10
- libgnutls30
- libgpg-error0
- libgssapi-krb5-2
- libhogweed5
- libidn2-0
- libjson-c4
- libk5crypto3
- libkeyutils1
- libkrb5-3
- libkrb5support0
- liblz4-1
- liblzma5
- libmount1
- libnettle7
- libp11-kit0
- libpcre2-8-0
- libselinux1
- libsqlite3-0
- libssl1.1
- libstdc++6
- libsystemd0
- libtasn1-6
- libudev1
- libunistring2
- libuuid1
- libwrap0
- libzstd1
- zlib1g
- libx11-xcb1
- libdrm2
- libgbm1
- libxcb-dri3-0
cleanup:
after: [antares]
plugin: nil
build-snaps: [gnome-3-28-1804]
build-snaps: [gnome-3-38-2004]
override-prime: |
set -eux
cd /snap/gnome-3-28-1804/current
cd /snap/gnome-3-38-2004/current
find . -type f,l -exec rm -f $SNAPCRAFT_PRIME/{} \;
mdns-lookup:
@@ -91,7 +136,7 @@ parts:
- 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
sed -Ee 's/^\s*hosts:(\s+)files/hosts:\1files mdns4_minimal \[NOTFOUND=return\]/' /snap/core20/current/etc/nsswitch.conf > $SNAPCRAFT_STAGE/etc/nsswitch.conf
snapcraftctl prime
prime:
- lib/$SNAPCRAFT_ARCH_TRIPLET/libnss_mdns4_minimal*
@@ -101,7 +146,7 @@ apps:
antares:
command: opt/Antares/antares --no-sandbox
desktop: usr/share/applications/antares.desktop
extensions: [gnome-3-28]
extensions: [gnome-3-38]
environment:
# Fallback to XWayland if running in a Wayland session.
DISABLE_WAYLAND: 1

View File

@@ -7,6 +7,7 @@ module.exports = {
database: false,
collations: false,
engines: false,
connectionSchema: false,
// Tools
processesList: false,
usersManagement: false,
@@ -21,6 +22,8 @@ module.exports = {
functions: false,
schedulers: false,
// Settings
elementsWrapper: '',
stringsWrapper: '"',
tableAdd: false,
viewAdd: false,
triggerAdd: false,
@@ -43,6 +46,7 @@ module.exports = {
unsigned: false,
nullable: false,
zerofill: false,
tableOptions: false,
autoIncrement: false,
comment: false,
collation: false,
@@ -62,8 +66,15 @@ module.exports = {
functionSql: false,
functionContext: false,
functionLanguage: false,
triggerMiltipleEvents: false,
triggerSql: false,
triggerStatementInCreation: false,
triggerMultipleEvents: false,
triggerTableInName: false,
triggerUpdateColumns: false,
triggerOnlyRename: false,
triggerEnableDisable: 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
@@ -20,6 +21,8 @@ module.exports = {
functions: true,
schedulers: true,
// Settings
elementsWrapper: '',
stringsWrapper: '"',
tableAdd: true,
viewAdd: true,
triggerAdd: true,
@@ -39,6 +42,7 @@ module.exports = {
unsigned: true,
nullable: true,
zerofill: true,
tableOptions: true,
autoIncrement: true,
comment: true,
collation: true,
@@ -51,6 +55,7 @@ module.exports = {
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',

View File

@@ -13,30 +13,43 @@ module.exports = {
// Structure
tables: true,
views: true,
triggers: false,
triggers: true,
triggerFunctions: true,
routines: true,
functions: true,
// Settings
elementsWrapper: '"',
stringsWrapper: '\'',
tableAdd: true,
viewAdd: true,
triggerAdd: false,
triggerAdd: true,
triggerFunctionAdd: true,
routineAdd: true,
functionAdd: true,
databaseEdit: false,
tableSettings: true,
viewSettings: true,
triggerSettings: false,
triggerSettings: true,
triggerFunctionSettings: true,
routineSettings: true,
functionSettings: true,
indexes: true,
foreigns: true,
nullable: true,
tableArray: true,
procedureSql: '$BODY$\r\n\r\n$BODY$',
procedureSql: '$procedure$\r\n\r\n$procedure$',
procedureContext: true,
procedureLanguage: true,
functionSql: '$BODY$\r\n\r\n$BODY$',
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,
triggerEnableDisable: true,
languages: ['sql', 'plpgsql', 'c', 'internal']
};

View File

@@ -4,6 +4,7 @@ export const TEXT = [
'CHARACTER',
'CHARACTER VARYING'
];
export const LONG_TEXT = [
'TEXT',
'MEDIUMTEXT',
@@ -50,6 +51,7 @@ export const BOOLEAN = [
];
export const DATE = ['DATE'];
export const TIME = [
'TIME',
'TIME WITH TIME ZONE'

View File

@@ -1,106 +0,0 @@
'use strict';
import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path';
import { format as formatUrl } from 'url';
import Store from 'electron-store';
import ipcHandlers from './ipc-handlers';
Store.initRenderer();
const isDevelopment = process.env.NODE_ENV !== 'production';
const gotTheLock = app.requestSingleInstanceLock();
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
// global reference to mainWindow (necessary to prevent window from being garbage collected)
let mainWindow;
async function createMainWindow () {
const icon = require('../renderer/images/logo-32.png');
const window = new BrowserWindow({
width: 1024,
height: 800,
minWidth: 900,
minHeight: 550,
title: 'Antares',
autoHideMenuBar: true,
icon: nativeImage.createFromDataURL(icon.default),
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
'web-security': false,
enableRemoteModule: true,
spellcheck: false
},
frame: false,
backgroundColor: '#1d1d1d'
});
try {
if (isDevelopment) {
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer');
const toolName = await installExtension(VUEJS_DEVTOOLS);
console.log(toolName, 'installed');
}
else {
await window.loadURL(formatUrl({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file',
slashes: true
}));
}
}
catch (err) {
console.log(err);
}
window.on('closed', () => {
mainWindow = null;
});
window.webContents.on('devtools-opened', () => {
window.focus();
setImmediate(() => {
window.focus();
});
});
return window;
};
if (!gotTheLock)
app.quit();
else {
require('@electron/remote/main').initialize();
// Initialize ipcHandlers
ipcHandlers();
// quit application when all windows are closed
app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits
if (process.platform !== 'darwin')
app.quit();
});
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 = await createMainWindow();
if (isDevelopment)
mainWindow.webContents.openDevTools();
}
});
// create main BrowserWindow when electron is ready
app.on('ready', async () => {
mainWindow = await createMainWindow();
if (isDevelopment)
mainWindow.webContents.openDevTools();
});
}

View File

@@ -24,14 +24,24 @@ 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,
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
passphrase: conn.sshPassphrase
};
}
try {
const connection = await ClientsFactory.getConnection({
client: conn.client,
params
});
await connection.connect();
await connection.select('1+1').run();
connection.destroy();
@@ -58,6 +68,9 @@ export default connections => {
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,
@@ -67,6 +80,17 @@ export default connections => {
};
}
if (conn.ssh) {
params.ssh = {
host: conn.sshHost,
username: conn.sshUser,
password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22,
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
passphrase: conn.sshPassphrase
};
}
try {
const connection = ClientsFactory.getConnection({
client: conn.client,

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

@@ -40,4 +40,17 @@ export default (connections) => {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('toggle-scheduler', async (event, params) => {
try {
if (!params.enabled)
await connections[params.uid].enableEvent({ ...params });
else
await connections[params.uid].disableEvent({ ...params });
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -112,6 +112,17 @@ export default connections => {
}
});
ipcMain.handle('kill-process', async (event, { uid, pid }) => {
try {
const result = await connections[uid].killProcess(pid);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('use-schema', async (event, { uid, schema }) => {
if (!schema) return;
@@ -128,7 +139,12 @@ export default connections => {
if (!query) return;
try {
const result = await connections[uid].raw(query, { nest: true, details: true, schema });
const result = await connections[uid].raw(query, {
nest: true,
details: true,
schema,
comments: false
});
return { status: 'success', response: result };
}

View File

@@ -16,19 +16,44 @@ 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, where }) => {
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() });
const result = await query.run({ details: true });
if (where)
query.where(where);
const result = await query.run({ details: true, schema });
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-table-count', async (event, params) => {
try {
const result = await connections[params.uid].getTableApproximateCount(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-table-options', async (event, params) => {
try {
const result = await connections[params.uid].getTableOptions(params);
return { status: 'success', response: result };
}
catch (err) {
@@ -59,6 +84,8 @@ export default (connections) => {
});
ipcMain.handle('update-table-cell', async (event, params) => {
delete params.row._antares_id;
try { // TODO: move to client classes
let escapedParam;
let reload = false;
@@ -420,6 +447,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

@@ -40,4 +40,17 @@ export default (connections) => {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('toggle-trigger', async (event, params) => {
try {
if (!params.enabled)
await connections[params.uid].enableTrigger(params);
else
await connections[params.uid].disableTrigger(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
const isMacOS = process.platform === 'darwin';
let mainWindow;
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
@@ -9,8 +10,11 @@ autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
export default () => {
ipcMain.on('check-for-updates', event => {
mainWindow = event;
if (process.windowsStore)
if (process.windowsStore || (process.platform === 'linux' && !process.env.APPIMAGE))
mainWindow.reply('no-auto-update');
else if (isMacOS) { // Temporary solution on MacOS for unsigned app updates
autoUpdater.autoDownload = false;
}
else {
autoUpdater.checkForUpdatesAndNotify().catch(() => {
mainWindow.reply('check-failed');
@@ -28,7 +32,10 @@ export default () => {
});
autoUpdater.on('update-available', () => {
mainWindow.reply('update-available');
if (isMacOS)
mainWindow.reply('link-to-download');
else
mainWindow.reply('update-available');
});
autoUpdater.on('update-not-available', () => {

View File

@@ -16,6 +16,7 @@ export class AntaresCore {
this._params = args.params;
this._poolSize = args.poolSize || false;
this._connection = null;
this._ssh = null;
this._logger = args.logger || console.log;
this._queryDefaults = {
@@ -26,6 +27,7 @@ export class AntaresCore {
groupBy: [],
orderBy: [],
limit: [],
offset: [],
join: [],
update: [],
insert: [],
@@ -109,6 +111,11 @@ export class AntaresCore {
return this;
}
offset (...args) {
this._query.offset = args;
return this;
}
/**
* @param {String | Array} args field = value
* @returns
@@ -134,7 +141,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

@@ -2,6 +2,12 @@
import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
const queryLogger = sql => {
// Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql);
};
export class ClientsFactory {
/**
* Returns a database connection based on received args.
@@ -12,11 +18,19 @@ 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
*/
static getConnection (args) {
args.logger = queryLogger;
switch (args.client) {
case 'mysql':
case 'maria':

View File

@@ -2,6 +2,7 @@
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) {
@@ -104,11 +105,39 @@ export class MySQLClient extends AntaresCore {
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) {
try {
this._ssh = new SSH2Promise({ ...this._params.ssh });
const tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = tunnel.localPort;
}
catch (err) {
if (this._ssh) this._ssh.close();
throw err;
}
}
if (!this._poolSize)
this._connection = await mysql.createConnection(this._params);
this._connection = await mysql.createConnection(dbConfig);
else {
this._connection = mysql.createPool({
...this._params,
...dbConfig,
connectionLimit: this._poolSize,
typeCast: (field, next) => {
if (field.type === 'DATETIME')
@@ -125,6 +154,7 @@ export class MySQLClient extends AntaresCore {
*/
destroy () {
this._connection.end();
if (this._ssh) this._ssh.close();
}
/**
@@ -145,14 +175,21 @@ 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`');
const tablesArr = [];
const triggersArr = [];
let schemaSize = 0;
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}\``);
@@ -174,7 +211,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 => {
@@ -188,6 +225,9 @@ export class MySQLClient extends AntaresCore {
break;
}
const tableSize = table.Data_length + table.Index_length;
schemaSize += tableSize;
return {
name: table.Name,
type: tableType,
@@ -196,7 +236,7 @@ export class MySQLClient extends AntaresCore {
updated: table.Update_time,
engine: table.Engine,
comment: table.Comment,
size: table.Data_length + table.Index_length,
size: tableSize,
autoIncrement: table.Auto_increment,
collation: table.Collation
};
@@ -240,7 +280,7 @@ export class MySQLClient extends AntaresCore {
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS,
ends: scheduler.ENDS,
status: scheduler.STATUS,
enabled: scheduler.STATUS === 'ENABLED',
executeAt: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD,
intervalValue: scheduler.INTERVAL_VALUE,
@@ -273,6 +313,7 @@ export class MySQLClient extends AntaresCore {
return {
name: db.Database,
size: schemaSize,
tables: remappedTables,
functions: remappedFunctions,
procedures: remappedProcedures,
@@ -283,6 +324,7 @@ export class MySQLClient extends AntaresCore {
else {
return {
name: db.Database,
size: 0,
tables: [],
functions: [],
procedures: [],
@@ -309,9 +351,11 @@ export class MySQLClient extends AntaresCore {
.orderBy({ ORDINAL_POSITION: 'ASC' })
.run();
const { rows: fields } = await this.raw(`SHOW CREATE TABLE ${schema}.${table}`);
const { rows: fields } = await this.raw(`SHOW CREATE TABLE \`${schema}\`.\`${table}\``);
const remappedFields = fields.map(row => {
if (!row['Create Table']) return false;
let n = 0;
return row['Create Table']
.split('')
@@ -330,7 +374,16 @@ export class MySQLClient extends AntaresCore {
if (nameAndType[0].charAt(0) !== '`') return false;
const details = fieldArr.slice(2).join(' ');
const defaultValue = details.includes('DEFAULT') ? details.match(/(?<=DEFAULT ).*?$/gs)[0] : null;
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 {
@@ -361,7 +414,7 @@ export class MySQLClient extends AntaresCore {
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: remappedFields[field.COLUMN_NAME].type,
type: remappedFields ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE,
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION,
@@ -373,7 +426,7 @@ export class MySQLClient extends AntaresCore {
unsigned: field.COLUMN_TYPE.includes('unsigned'),
zerofill: field.COLUMN_TYPE.includes('zerofill'),
order: field.ORDINAL_POSITION,
default: remappedFields[field.COLUMN_NAME].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'),
@@ -383,6 +436,56 @@ export class MySQLClient extends AntaresCore {
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table row count
* @memberof MySQLClient
*/
async getTableApproximateCount ({ schema, table }) {
const { rows } = await this.raw(`SELECT table_rows "count" FROM information_schema.tables WHERE table_name = "${table}" AND table_schema = "${schema}"`);
return rows.length ? rows[0].count : 0;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table options
* @memberof MySQLClient
*/
async getTableOptions ({ schema, table }) {
const { rows } = await this.raw(`SHOW TABLE STATUS FROM \`${schema}\` WHERE Name = '${table}'`);
if (rows.length) {
let tableType;
switch (rows[0].Comment) {
case 'VIEW':
tableType = 'view';
break;
default:
tableType = 'table';
break;
}
return {
name: rows[0].Name,
type: tableType,
rows: rows[0].Rows,
created: rows[0].Create_time,
updated: rows[0].Update_time,
engine: rows[0].Engine,
comment: rows[0].Comment,
size: rows[0].Data_length + rows[0].Index_length,
autoIncrement: rows[0].Auto_increment,
collation: rows[0].Collation
};
};
return {};
}
/**
* @param {Object} params
* @param {String} params.schema
@@ -532,7 +635,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropView (params) {
const sql = `DROP VIEW \`${this._schema}\`.\`${params.view}\``;
const sql = `DROP VIEW \`${params.schema}\`.\`${params.view}\``;
return await this.raw(sql);
}
@@ -544,10 +647,15 @@ 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 \`${this._schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
let sql = `
USE \`${view.schema}\`;
ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''}
SQL SECURITY ${view.security}
VIEW \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}
`;
if (view.name !== view.oldName)
sql += `; RENAME TABLE \`${this._schema}\`.\`${view.oldName}\` TO \`${this._schema}\`.\`${view.name}\``;
sql += `; RENAME TABLE \`${view.schema}\`.\`${view.oldName}\` TO \`${view.schema}\`.\`${view.name}\``;
return await this.raw(sql);
}
@@ -558,8 +666,8 @@ export class MySQLClient extends AntaresCore {
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createView (view) {
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` : ''}`;
async createView (params) {
const sql = `CREATE ALGORITHM = ${params.algorithm} ${params.definer ? `DEFINER=${params.definer} ` : ''}SQL SECURITY ${params.security} VIEW \`${params.schema}\`.\`${params.name}\` AS ${params.sql} ${params.updateOption ? `WITH ${params.updateOption} CHECK OPTION` : ''}`;
return await this.raw(sql);
}
@@ -579,8 +687,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];
}
@@ -592,7 +700,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropTrigger (params) {
const sql = `DROP TRIGGER \`${this._schema}\`.\`${params.trigger}\``;
const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``;
return await this.raw(sql);
}
@@ -609,8 +717,8 @@ export class MySQLClient extends AntaresCore {
try {
await this.createTrigger(tempTrigger);
await this.dropTrigger({ trigger: tempTrigger.name });
await this.dropTrigger({ trigger: trigger.oldName });
await this.dropTrigger({ schema: trigger.schema, trigger: tempTrigger.name });
await this.dropTrigger({ schema: trigger.schema, trigger: trigger.oldName });
await this.createTrigger(trigger);
}
catch (err) {
@@ -624,8 +732,8 @@ export class MySQLClient extends AntaresCore {
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createTrigger (trigger) {
const sql = `CREATE ${trigger.definer ? `DEFINER=${trigger.definer} ` : ''}TRIGGER \`${this._schema}\`.\`${trigger.name}\` ${trigger.event1} ${trigger.event2} ON \`${trigger.table}\` FOR EACH ROW ${trigger.sql}`;
async createTrigger (params) {
const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}TRIGGER \`${params.schema}\`.\`${params.name}\` ${params.activation} ${params.event} ON \`${params.table}\` FOR EACH ROW ${params.sql}`;
return await this.raw(sql, { split: false });
}
@@ -699,7 +807,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropRoutine (params) {
const sql = `DROP PROCEDURE \`${this._schema}\`.\`${params.routine}\``;
const sql = `DROP PROCEDURE \`${params.schema}\`.\`${params.routine}\``;
return await this.raw(sql);
}
@@ -716,8 +824,8 @@ export class MySQLClient extends AntaresCore {
try {
await this.createRoutine(tempProcedure);
await this.dropRoutine({ routine: tempProcedure.name });
await this.dropRoutine({ routine: routine.oldName });
await this.dropRoutine({ schema: routine.schema, routine: tempProcedure.name });
await this.dropRoutine({ schema: routine.schema, routine: routine.oldName });
await this.createRoutine(routine);
}
catch (err) {
@@ -731,21 +839,21 @@ export class MySQLClient extends AntaresCore {
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createRoutine (routine) {
const parameters = 'parameters' in routine
? routine.parameters.reduce((acc, curr) => {
async createRoutine (params) {
const parameters = 'parameters' in params
? params.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',')
: '';
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${this._schema}\`.\`${routine.name}\`(${parameters})
const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}PROCEDURE \`${params.schema}\`.\`${params.name}\`(${parameters})
LANGUAGE SQL
${routine.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${routine.dataAccess}
SQL SECURITY ${routine.security}
COMMENT '${routine.comment}'
${routine.sql}`;
${params.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${params.dataAccess}
SQL SECURITY ${params.security}
COMMENT '${params.comment}'
${params.sql}`;
return await this.raw(sql, { split: false });
}
@@ -826,7 +934,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropFunction (params) {
const sql = `DROP FUNCTION \`${this._schema}\`.\`${params.func}\``;
const sql = `DROP FUNCTION \`${params.schema}\`.\`${params.func}\``;
return await this.raw(sql);
}
@@ -843,8 +951,8 @@ export class MySQLClient extends AntaresCore {
try {
await this.createFunction(tempProcedure);
await this.dropFunction({ func: tempProcedure.name });
await this.dropFunction({ func: func.oldName });
await this.dropFunction({ schema: func.schema, func: tempProcedure.name });
await this.dropFunction({ schema: func.schema, func: func.oldName });
await this.createFunction(func);
}
catch (err) {
@@ -858,20 +966,22 @@ export class MySQLClient extends AntaresCore {
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createFunction (func) {
const parameters = func.parameters.reduce((acc, curr) => {
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
async createFunction (params) {
const parameters = 'parameters' in params
? params.parameters.reduce((acc, curr) => {
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',')
: '';
const body = func.returns ? func.sql : 'BEGIN\n RETURN 0;\nEND';
const body = params.returns ? params.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})` : ''}
const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}FUNCTION \`${params.schema}\`.\`${params.name}\`(${parameters}) RETURNS ${params.returns || 'SMALLINT'}${params.returnsLength ? `(${params.returnsLength})` : ''}
LANGUAGE SQL
${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${func.dataAccess}
SQL SECURITY ${func.security}
COMMENT '${func.comment}'
${params.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${params.dataAccess}
SQL SECURITY ${params.security}
COMMENT '${params.comment}'
${body}`;
return await this.raw(sql, { split: false });
@@ -918,7 +1028,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropEvent (params) {
const sql = `DROP EVENT \`${this._schema}\`.\`${params.scheduler}\``;
const sql = `DROP EVENT \`${params.schema}\`.\`${params.scheduler}\``;
return await this.raw(sql);
}
@@ -934,13 +1044,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 \`${this._schema}\`.\`${scheduler.oldName}\`
const sql = `ALTER ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.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 \`${this._schema}\`.\`${scheduler.name}\`` : ''}
${scheduler.name !== scheduler.oldName ? `RENAME TO \`${scheduler.schema}\`.\`${scheduler.name}\`` : ''}
${scheduler.state}
COMMENT '${scheduler.comment}'
DO ${scheduler.sql}`;
@@ -954,20 +1064,30 @@ export class MySQLClient extends AntaresCore {
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createEvent (scheduler) {
const sql = `CREATE ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${this._schema}\`.\`${scheduler.name}\`
async createEvent (params) {
const sql = `CREATE ${params.definer ? ` DEFINER=${params.definer}` : ''} EVENT \`${params.schema}\`.\`${params.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}`;
${params.execution === 'EVERY'
? `EVERY ${params.every.join(' ')}${params.starts ? ` STARTS '${params.starts}'` : ''}${params.ends ? ` ENDS '${params.ends}'` : ''}`
: `AT '${params.at}'`}
ON COMPLETION${!params.preserve ? ' NOT' : ''} PRESERVE
${params.state}
COMMENT '${params.comment}'
DO ${params.sql}`;
return await this.raw(sql, { split: false });
}
async enableEvent ({ schema, scheduler }) {
const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` ENABLE`;
return await this.raw(sql, { split: false });
}
async disableEvent ({ schema, scheduler }) {
const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` DISABLE`;
return await this.raw(sql, { split: false });
}
/**
* SHOW COLLATION
*
@@ -1078,6 +1198,10 @@ export class MySQLClient extends AntaresCore {
});
}
async killProcess (id) {
return await this.raw(`KILL ${id}`);
}
/**
* CREATE TABLE
*
@@ -1086,13 +1210,56 @@ export class MySQLClient extends AntaresCore {
*/
async createTable (params) {
const {
name,
collation,
comment,
engine
schema,
fields,
foreigns,
indexes,
options
} = params;
const newColumns = [];
const newIndexes = [];
const newForeigns = [];
const sql = `CREATE TABLE \`${this._schema}\`.\`${name}\` (\`${name}_ID\` INT NULL) COMMENT='${comment}', COLLATE='${collation}', ENGINE=${engine}`;
let sql = `CREATE TABLE \`${schema}\`.\`${options.name}\``;
// ADD FIELDS
fields.forEach(field => {
const typeInfo = this._getTypeInfo(field.type);
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`\`${field.name}\`
${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
${field.autoIncrement ? 'AUTO_INCREMENT' : ''}
${field.default ? `DEFAULT ${field.default}` : ''}
${field.comment ? `COMMENT '${field.comment}'` : ''}
${field.collation ? `COLLATE ${field.collation}` : ''}
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
});
// ADD INDEX
indexes.forEach(index => {
const fields = index.fields.map(field => `\`${field}\``).join(',');
let type = index.type;
if (type === 'PRIMARY')
newIndexes.push(`PRIMARY KEY (${fields})`);
else {
if (type === 'UNIQUE')
type = 'UNIQUE INDEX';
newIndexes.push(`${type} \`${index.name}\` (${fields})`);
}
});
// ADD FOREIGN KEYS
foreigns.forEach(foreign => {
newForeigns.push(`CONSTRAINT \`${foreign.constraintName}\` FOREIGN KEY (\`${foreign.field}\`) REFERENCES \`${foreign.refTable}\` (\`${foreign.refField}\`) ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
});
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')}) COMMENT='${options.comment}', COLLATE='${options.collation}', ENGINE=${options.engine}`;
return await this.raw(sql);
}
@@ -1106,6 +1273,7 @@ export class MySQLClient extends AntaresCore {
async alterTable (params) {
const {
table,
schema,
additions,
deletions,
changes,
@@ -1114,7 +1282,7 @@ export class MySQLClient extends AntaresCore {
options
} = params;
let sql = `ALTER TABLE \`${this._schema}\`.\`${table}\` `;
let sql = `ALTER TABLE \`${schema}\`.\`${table}\` `;
const alterColumns = [];
// OPTIONS
@@ -1226,11 +1394,22 @@ export class MySQLClient extends AntaresCore {
sql += alterColumns.join(', ');
// RENAME
if (options.name) sql += `; RENAME TABLE \`${this._schema}\`.\`${table}\` TO \`${this._schema}\`.\`${options.name}\``;
if (options.name) sql += `; RENAME TABLE \`${schema}\`.\`${table}\` TO \`${schema}\`.\`${options.name}\``;
return await this.raw(sql);
}
/**
* DUPLICATE TABLE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async duplicateTable (params) {
const sql = `CREATE TABLE \`${params.schema}\`.\`${params.table}_copy\` LIKE \`${params.schema}\`.\`${params.table}\``;
return await this.raw(sql);
}
/**
* TRUNCATE TABLE
*
@@ -1238,7 +1417,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async truncateTable (params) {
const sql = `TRUNCATE TABLE \`${this._schema}\`.\`${params.table}\``;
const sql = `TRUNCATE TABLE \`${params.schema}\`.\`${params.table}\``;
return await this.raw(sql);
}
@@ -1249,7 +1428,7 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async dropTable (params) {
const sql = `DROP TABLE \`${this._schema}\`.\`${params.table}\``;
const sql = `DROP TABLE \`${params.schema}\`.\`${params.table}\``;
return await this.raw(sql);
}
@@ -1304,7 +1483,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}`;
}
/**
@@ -1323,13 +1505,21 @@ export class MySQLClient extends AntaresCore {
nest: false,
details: false,
split: true,
comments: true,
...args
};
if (!args.comments)
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
const nestTables = args.nest ? '.' : false;
const resultsArr = [];
let paramsArr = [];
const queries = args.split ? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) : [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;
@@ -1358,7 +1548,7 @@ export class MySQLClient extends AntaresCore {
name: field.orgName,
alias: field.name,
orgName: field.orgName,
schema: field.schema,
schema: args.schema || field.schema,
table: field.table,
tableAlias: field.table,
orgTable: field.orgTable,
@@ -1393,6 +1583,7 @@ export class MySQLClient extends AntaresCore {
});
}
catch (err) {
if (isPool) connection.release();
reject(err);
}
@@ -1401,6 +1592,7 @@ export class MySQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
if (isPool) connection.release();
reject(err);
}
}
@@ -1414,7 +1606,10 @@ export class MySQLClient extends AntaresCore {
fields: remappedFields,
keys: keysArr
});
}).catch(reject);
}).catch((err) => {
if (isPool) connection.release();
reject(err);
});
});
resultsArr.push({ rows, report, fields, keys, duration });

View File

@@ -3,11 +3,13 @@ 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();
}
types.setTypeParser(20, a => parseInt(a));// bigint string to number
types.setTypeParser(1082, pgToString); // date
types.setTypeParser(1083, pgToString); // time
types.setTypeParser(1114, pgToString); // timestamp
@@ -35,6 +37,26 @@ export class PostgreSQLClient extends AntaresCore {
};
}
_reducer (acc, curr) {
const type = typeof curr;
switch (type) {
case 'number':
case 'string':
return [...acc, curr];
case 'object':
if (Array.isArray(curr))
return [...acc, ...curr];
else {
const clausoles = [];
for (const key in curr)
clausoles.push(`"${key}" ${curr[key]}`);
return clausoles;
}
}
}
_getTypeInfo (type) {
return dataTypes
.reduce((acc, group) => [...acc, ...group.types], [])
@@ -51,13 +73,41 @@ export class PostgreSQLClient extends AntaresCore {
* @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) {
try {
this._ssh = new SSH2Promise({ ...this._params.ssh });
const tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = tunnel.localPort;
}
catch (err) {
if (this._ssh) this._ssh.close();
throw err;
}
}
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;
}
}
@@ -67,10 +117,11 @@ export class PostgreSQLClient extends AntaresCore {
*/
destroy () {
this._connection.end();
if (this._ssh) this._ssh.close();
}
/**
* Executes an USE query
* Executes an "USE" query
*
* @param {String} schema
* @memberof PostgreSQLClient
@@ -78,7 +129,7 @@ export class PostgreSQLClient extends AntaresCore {
use (schema) {
this._schema = schema;
if (schema)
return this.raw(`SET search_path TO ${schema}`);
return this.raw(`SET search_path TO "${schema}"`);
}
/**
@@ -93,6 +144,7 @@ export class PostgreSQLClient extends AntaresCore {
const tablesArr = [];
const triggersArr = [];
let schemaSize = 0;
for (const db of databases) {
if (!schemas.has(db.database)) continue;
@@ -118,19 +170,20 @@ export class PostgreSQLClient extends AntaresCore {
}
let { rows: triggers } = 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
SELECT
pg_class.relname AS table_name,
pg_trigger.tgname AS trigger_name,
pg_namespace.nspname AS trigger_schema,
(pg_trigger.tgenabled != 'D')::bool AS enabled
FROM pg_trigger
JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
JOIN information_schema.triggers ON information_schema.triggers.trigger_schema = pg_namespace.nspname
AND information_schema.triggers.event_object_table = pg_class.relname
AND information_schema.triggers.trigger_name = pg_trigger.tgname
WHERE trigger_schema = '${db.database}'
GROUP BY 1,2,3,4,6,7,8
ORDER BY table_schema,
table_name
GROUP BY 1, 2, 3, 4
ORDER BY table_name
`);
if (triggers.length) {
@@ -146,11 +199,14 @@ export class PostgreSQLClient extends AntaresCore {
if (schemas.has(db.database)) {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.database).map(table => {
const tableSize = +table.data_length + table.index_length;
schemaSize += tableSize;
return {
name: table.table_name,
type: table.table_type === 'VIEW' ? 'view' : 'table',
rows: table.reltuples,
size: +table.data_length + +table.index_length,
size: tableSize,
collation: table.Collation,
comment: table.comment,
engine: ''
@@ -189,17 +245,16 @@ export class PostgreSQLClient extends AntaresCore {
return {
name: `${trigger.table_name}.${trigger.trigger_name}`,
orgName: trigger.trigger_name,
timing: trigger.activation,
definer: '',
definition: trigger.definition,
event: trigger.event,
table: trigger.table_name,
sqlMode: ''
sqlMode: '',
enabled: trigger.enabled
};
});
return {
name: db.database,
size: schemaSize,
tables: remappedTables,
functions: remappedFunctions,
procedures: remappedProcedures,
@@ -211,10 +266,12 @@ export class PostgreSQLClient extends AntaresCore {
else {
return {
name: db.database,
size: 0,
tables: [],
functions: [],
procedures: [],
triggers: [],
triggerFunctions: [],
schedulers: []
};
}
@@ -268,6 +325,53 @@ export class PostgreSQLClient extends AntaresCore {
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table row count
* @memberof PostgreSQLClient
*/
async getTableApproximateCount ({ schema, table }) {
const { rows } = await this.raw(`SELECT reltuples AS count FROM pg_class WHERE relname = '${table}'`);
return rows.length ? rows[0].count : 0;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table options
* @memberof MySQLClient
*/
async getTableOptions ({ schema, table }) {
const { rows } = await this.raw(`
SELECT *,
pg_table_size(QUOTE_IDENT(t.TABLE_SCHEMA) || '.' || QUOTE_IDENT(t.TABLE_NAME))::bigint AS data_length,
pg_relation_size(QUOTE_IDENT(t.TABLE_SCHEMA) || '.' || QUOTE_IDENT(t.TABLE_NAME))::bigint AS index_length,
c.reltuples, obj_description(c.oid) AS comment
FROM "information_schema"."tables" AS t
LEFT JOIN "pg_namespace" n ON t.table_schema = n.nspname
LEFT JOIN "pg_class" c ON n.oid = c.relnamespace AND c.relname=t.table_name
WHERE t."table_schema" = '${schema}'
AND table_name = '${table}'
`);
if (rows.length) {
return {
name: rows[0].table_name,
type: rows[0].table_type === 'VIEW' ? 'view' : 'table',
rows: rows[0].reltuples,
size: +rows[0].data_length + +rows[0].index_length,
collation: rows[0].Collation,
comment: rows[0].comment,
engine: ''
};
};
return {};
}
/**
* @param {Object} params
* @param {String} params.schema
@@ -458,7 +562,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropView (params) {
const sql = `DROP VIEW ${this._schema}.${params.view}`;
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
@@ -470,10 +574,10 @@ export class PostgreSQLClient extends AntaresCore {
*/
async alterView (params) {
const { view } = params;
let sql = `CREATE OR REPLACE VIEW ${this._schema}.${view.oldName} AS ${view.sql}`;
let sql = `CREATE OR REPLACE VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`;
if (view.name !== view.oldName)
sql += `; ALTER VIEW ${this._schema}.${view.oldName} RENAME TO ${view.name}`;
sql += `; ALTER VIEW "${view.schema}"."${view.oldName}" RENAME TO "${view.name}"`;
return await this.raw(sql);
}
@@ -484,8 +588,8 @@ export class PostgreSQLClient extends AntaresCore {
* @returns {Array.<Object>} parameters
* @memberof PostgreSQLClient
*/
async createView (view) {
const sql = `CREATE VIEW ${this._schema}.${view.name} AS ${view.sql}`;
async createView (params) {
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql);
}
@@ -496,17 +600,39 @@ 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
information_schema.triggers.event_object_schema AS table_schema,
information_schema.triggers.event_object_table AS table_name,
information_schema.triggers.trigger_schema,
information_schema.triggers.trigger_name,
string_agg(event_manipulation, ',') AS EVENT,
action_timing AS activation,
action_condition AS condition,
action_statement AS definition,
(pg_trigger.tgenabled != 'D')::bool AS enabled
FROM pg_trigger
JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
JOIN information_schema.triggers ON pg_namespace.nspname = information_schema.triggers.trigger_schema
AND pg_class.relname = information_schema.triggers.event_object_table
WHERE trigger_schema = '${schema}'
AND trigger_name = '${triggerName}'
AND event_object_table = '${table}'
GROUP BY 1,2,3,4,6,7,8,9
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: [...new Set(row.event.split(','))],
activation: row.activation
};
})[0];
}
@@ -519,7 +645,7 @@ export class PostgreSQLClient extends AntaresCore {
*/
async dropTrigger (params) {
const triggerParts = params.trigger.split('.');
const sql = `DROP TRIGGER ${triggerParts[1]} ON ${triggerParts[0]}`;
const sql = `DROP TRIGGER "${triggerParts[1]}" ON "${params.schema}"."${triggerParts[0]}"`;
return await this.raw(sql);
}
@@ -536,8 +662,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({ schema: trigger.schema, trigger: `${tempTrigger.table}.${tempTrigger.name}` });
await this.dropTrigger({ schema: trigger.schema, trigger: `${trigger.table}.${trigger.oldName}` });
await this.createTrigger(trigger);
}
catch (err) {
@@ -551,8 +677,21 @@ export class PostgreSQLClient extends AntaresCore {
* @returns {Array.<Object>} parameters
* @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}`;
async createTrigger (params) {
const eventsString = Array.isArray(params.event) ? params.event.join(' OR ') : params.event;
const sql = `CREATE TRIGGER "${params.name}" ${params.activation} ${eventsString} ON "${params.schema}"."${params.table}" FOR EACH ROW ${params.sql}`;
return await this.raw(sql, { split: false });
}
async enableTrigger ({ schema, trigger }) {
const [table, triggerName] = trigger.split('.');
const sql = `ALTER TABLE "${schema}"."${table}" ENABLE TRIGGER "${triggerName}"`;
return await this.raw(sql, { split: false });
}
async disableTrigger ({ schema, trigger }) {
const [table, triggerName] = trigger.split('.');
const sql = `ALTER TABLE "${schema}"."${table}" DISABLE TRIGGER "${triggerName}"`;
return await this.raw(sql, { split: false });
}
@@ -595,6 +734,7 @@ export class PostgreSQLClient extends AntaresCore {
AND proc.routine_type = 'PROCEDURE'
AND proc.routine_name = '${routine}'
AND proc.specific_schema = '${schema}'
AND args.data_type != NULL
ORDER BY procedure_schema,
specific_name,
procedure_name,
@@ -606,7 +746,7 @@ export class PostgreSQLClient extends AntaresCore {
const parameters = results.rows.map(row => {
return {
name: row.parameter_name,
type: row.data_type.toUpperCase(),
type: row.data_type ? row.data_type.toUpperCase() : '',
length: '',
context: row.parameter_mode
};
@@ -633,7 +773,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropRoutine (params) {
const sql = `DROP PROCEDURE ${this._schema}.${params.routine}`;
const sql = `DROP PROCEDURE "${params.schema}"."${params.routine}"`;
return await this.raw(sql);
}
@@ -650,8 +790,8 @@ export class PostgreSQLClient extends AntaresCore {
try {
await this.createRoutine(tempProcedure);
await this.dropRoutine({ routine: tempProcedure.name });
await this.dropRoutine({ routine: routine.oldName });
await this.dropRoutine({ schema: routine.schema, routine: tempProcedure.name });
await this.dropRoutine({ schema: routine.schema, routine: routine.oldName });
await this.createRoutine(routine);
}
catch (err) {
@@ -668,15 +808,15 @@ 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}`);
return acc;
}, []).join(',')
: '';
if (this._schema !== 'public')
await this.use(this._schema);
if (routine.schema !== 'public')
await this.use(routine.schema);
const sql = `CREATE PROCEDURE ${this._schema}.${routine.name}(${parameters})
const sql = `CREATE PROCEDURE "${routine.schema}"."${routine.name}"(${parameters})
LANGUAGE ${routine.language}
SECURITY ${routine.security}
AS ${routine.sql}`;
@@ -762,7 +902,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropFunction (params) {
const sql = `DROP FUNCTION ${this._schema}.${params.func}`;
const sql = `DROP FUNCTION "${params.schema}"."${params.func}"`;
return await this.raw(sql);
}
@@ -779,8 +919,8 @@ export class PostgreSQLClient extends AntaresCore {
try {
await this.createFunction(tempProcedure);
await this.dropFunction({ func: tempProcedure.name });
await this.dropFunction({ func: func.oldName });
await this.dropFunction({ schema: func.schema, func: tempProcedure.name });
await this.dropFunction({ schema: func.schema, func: func.oldName });
await this.createFunction(func);
}
catch (err) {
@@ -797,17 +937,17 @@ export class PostgreSQLClient extends AntaresCore {
async createFunction (func) {
const parameters = 'parameters' in func
? func.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} ${curr.name} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
acc.push(`${curr.context} ${curr.name || ''} ${curr.type}`);
return acc;
}, []).join(',')
: '';
if (this._schema !== 'public')
await this.use(this._schema);
if (func.schema !== 'public')
await this.use(func.schema);
const body = func.returns ? func.sql : '$BODY$\n$BODY$';
const body = func.returns ? func.sql : '$function$\n$function$';
const sql = `CREATE FUNCTION ${this._schema}.${func.name}(${parameters})
const sql = `CREATE FUNCTION "${func.schema}"."${func.name}" (${parameters})
RETURNS ${func.returns || 'void'}
LANGUAGE ${func.language}
SECURITY ${func.security}
@@ -816,6 +956,48 @@ export class PostgreSQLClient extends AntaresCore {
return await this.raw(sql, { split: false });
}
/**
* ALTER TRIGGER FUNCTION
*
* @returns {Array.<Object>} parameters
* @memberof PostgreSQLClient
*/
async alterTriggerFunction (params) {
const { func } = params;
if (func.schema !== 'public')
await this.use(func.schema);
const body = func.returns ? func.sql : '$function$\n$function$';
const sql = `CREATE OR REPLACE FUNCTION "${func.schema}"."${func.name}" ()
RETURNS TRIGGER
LANGUAGE ${func.language}
AS ${body}`;
return await this.raw(sql, { split: false });
}
/**
* CREATE TRIGGER FUNCTION
*
* @returns {Array.<Object>} parameters
* @memberof PostgreSQLClient
*/
async createTriggerFunction (func) {
if (func.schema !== 'public')
await this.use(func.schema);
const body = func.returns ? func.sql : '$function$\r\nBEGIN\r\n\r\nEND\r\n$function$';
const sql = `CREATE FUNCTION "${func.schema}"."${func.name}" ()
RETURNS TRIGGER
LANGUAGE ${func.language}
AS ${body}`;
return await this.raw(sql, { split: false });
}
/**
* SELECT * FROM pg_collation
*
@@ -897,6 +1079,10 @@ export class PostgreSQLClient extends AntaresCore {
});
}
async killProcess (id) {
return await this.raw(`SELECT pg_terminate_backend(${id})`);
}
/**
* CREATE TABLE
*
@@ -905,11 +1091,53 @@ export class PostgreSQLClient extends AntaresCore {
*/
async createTable (params) {
const {
name
schema,
fields,
foreigns,
indexes,
options
} = params;
const newColumns = [];
const newIndexes = [];
const manageIndexes = [];
const newForeigns = [];
const sql = `CREATE TABLE ${this._schema}.${name} (${name}_id INTEGER NULL); ALTER TABLE ${this._schema}.${name} DROP COLUMN ${name}_id`;
let sql = `CREATE TABLE "${schema}"."${options.name}"`;
// ADD FIELDS
fields.forEach(field => {
const typeInfo = this._getTypeInfo(field.type);
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`"${field.name}"
${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
${field.default ? `DEFAULT ${field.default}` : ''}
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
});
// ADD INDEX
indexes.forEach(index => {
const fields = index.fields.map(field => `${field}`).join(',');
const type = index.type;
if (type === 'PRIMARY')
newIndexes.push(`PRIMARY KEY (${fields})`);
else if (type === 'UNIQUE')
newIndexes.push(`CONSTRAINT "${index.name}" UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX "${index.name}" ON "${schema}"."${options.name}" (${fields})`);
});
// ADD FOREIGN KEYS
foreigns.forEach(foreign => {
newForeigns.push(`CONSTRAINT "${foreign.constraintName}" FOREIGN KEY ("${foreign.field}") REFERENCES "${schema}"."${foreign.refTable}" ("${foreign.refField}") ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
});
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')})`;
if (manageIndexes.length) sql = `${sql}; ${manageIndexes.join(';')}`;
return await this.raw(sql);
}
@@ -922,6 +1150,7 @@ export class PostgreSQLClient extends AntaresCore {
async alterTable (params) {
const {
table,
schema,
additions,
deletions,
changes,
@@ -930,8 +1159,8 @@ export class PostgreSQLClient extends AntaresCore {
options
} = params;
if (this._schema !== 'public')
await this.use(this._schema);
if (schema !== 'public')
await this.use(schema);
let sql = '';
const alterColumns = [];
@@ -939,45 +1168,36 @@ export class PostgreSQLClient extends AntaresCore {
const createSequences = [];
const manageIndexes = [];
// OPTIONS
if ('comment' in options) alterColumns.push(`COMMENT='${options.comment}'`);
if ('engine' in options) alterColumns.push(`ENGINE=${options.engine}`);
if ('autoIncrement' in options) alterColumns.push(`AUTO_INCREMENT=${+options.autoIncrement}`);
if ('collation' in options) alterColumns.push(`COLLATE='${options.collation}'`);
// ADD FIELDS
additions.forEach(addition => {
const typeInfo = this._getTypeInfo(addition.type);
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN ${addition.name}
alterColumns.push(`ADD COLUMN "${addition.name}"
${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''}
${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'}
${addition.autoIncrement ? 'AUTO_INCREMENT' : ''}
${addition.default ? `DEFAULT ${addition.default}` : ''}
${addition.comment ? `COMMENT '${addition.comment}'` : ''}
${addition.collation ? `COLLATE ${addition.collation}` : ''}
${addition.onUpdate ? `ON UPDATE ${addition.onUpdate}` : ''}`);
});
// ADD INDEX
indexChanges.additions.forEach(addition => {
const fields = addition.fields.map(field => `${field}`).join(',');
const fields = addition.fields.map(field => `"${field}"`).join(',');
const type = addition.type;
if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${addition.name} UNIQUE (${fields})`);
alterColumns.push(`ADD CONSTRAINT "${addition.name}" UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX ${addition.name} ON ${this._schema}.${table}(${fields})`);
manageIndexes.push(`CREATE INDEX "${addition.name}" ON "${schema}"."${table}" (${fields})`);
});
// ADD FOREIGN KEYS
foreignChanges.additions.forEach(addition => {
alterColumns.push(`ADD CONSTRAINT ${addition.constraintName} FOREIGN KEY (${addition.field}) REFERENCES ${addition.refTable} (${addition.refField}) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
alterColumns.push(`ADD CONSTRAINT "${addition.constraintName}" FOREIGN KEY ("${addition.field}") REFERENCES "${schema}"."${addition.refTable}" (${addition.refField}) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
});
// CHANGE FIELDS
@@ -1003,6 +1223,7 @@ export class PostgreSQLClient extends AntaresCore {
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.name}"`);
@@ -1010,7 +1231,7 @@ export class PostgreSQLClient extends AntaresCore {
}
if (change.orgName !== change.name)
renameColumns.push(`ALTER TABLE "${this._schema}"."${table}" RENAME COLUMN "${change.orgName}" TO "${change.name}"`);
renameColumns.push(`ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${change.orgName}" TO "${change.name}"`);
});
// CHANGE INDEX
@@ -1020,45 +1241,45 @@ export class PostgreSQLClient extends AntaresCore {
else
manageIndexes.push(`DROP INDEX ${change.oldName}`);
const fields = change.fields.map(field => `${field}`).join(',');
const fields = change.fields.map(field => `"${field}"`).join(',');
const type = change.type;
if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${change.name} UNIQUE (${fields})`);
alterColumns.push(`ADD CONSTRAINT "${change.name}" UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX ${change.name} ON ${this._schema}.${table}(${fields})`);
manageIndexes.push(`CREATE INDEX "${change.name}" ON "${schema}"."${table}" (${fields})`);
});
// CHANGE FOREIGN KEYS
foreignChanges.changes.forEach(change => {
alterColumns.push(`DROP CONSTRAINT ${change.oldName}`);
alterColumns.push(`ADD CONSTRAINT ${change.constraintName} FOREIGN KEY (${change.field}) REFERENCES ${change.refTable} (${change.refField}) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
alterColumns.push(`DROP CONSTRAINT "${change.oldName}"`);
alterColumns.push(`ADD CONSTRAINT "${change.constraintName}" FOREIGN KEY (${change.field}) REFERENCES "${schema}"."${change.refTable}" ("${change.refField}") ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
});
// DROP FIELDS
deletions.forEach(deletion => {
alterColumns.push(`DROP COLUMN ${deletion.name}`);
alterColumns.push(`DROP COLUMN "${deletion.name}"`);
});
// DROP INDEX
indexChanges.deletions.forEach(deletion => {
if (['PRIMARY', 'UNIQUE'].includes(deletion.type))
alterColumns.push(`DROP CONSTRAINT ${deletion.name}`);
alterColumns.push(`DROP CONSTRAINT "${deletion.name}"`);
else
manageIndexes.push(`DROP INDEX ${deletion.name}`);
manageIndexes.push(`DROP INDEX "${deletion.name}"`);
});
// DROP FOREIGN KEYS
foreignChanges.deletions.forEach(deletion => {
alterColumns.push(`DROP CONSTRAINT ${deletion.constraintName}`);
alterColumns.push(`DROP CONSTRAINT "${deletion.constraintName}"`);
});
if (alterColumns.length) sql += `ALTER TABLE "${this._schema}"."${table}" ${alterColumns.join(', ')}; `;
if (alterColumns.length) sql += `ALTER TABLE "${schema}"."${table}" ${alterColumns.join(', ')}; `;
if (createSequences.length) sql = `${createSequences.join(';')}; ${sql}`;
if (manageIndexes.length) sql = `${manageIndexes.join(';')}; ${sql}`;
if (options.name) sql += `ALTER TABLE "${this._schema}"."${table}" RENAME TO "${options.name}"; `;
if (options.name) sql += `ALTER TABLE "${schema}"."${table}" RENAME TO "${options.name}"; `;
// RENAME
if (renameColumns.length) sql = `${renameColumns.join(';')}; ${sql}`;
@@ -1066,6 +1287,17 @@ export class PostgreSQLClient extends AntaresCore {
return await this.raw(sql);
}
/**
* DUPLICATE TABLE
*
* @returns {Array.<Object>} parameters
* @memberof PostgreSQLClient
*/
async duplicateTable (params) {
const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" (LIKE "${params.schema}"."${params.table}" INCLUDING ALL)`;
return await this.raw(sql);
}
/**
* TRUNCATE TABLE
*
@@ -1073,7 +1305,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async truncateTable (params) {
const sql = `TRUNCATE TABLE ${this._schema}.${params.table}`;
const sql = `TRUNCATE TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
@@ -1084,7 +1316,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropTable (params) {
const sql = `DROP TABLE ${this._schema}.${params.table}`;
const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
@@ -1108,7 +1340,7 @@ export class PostgreSQLClient extends AntaresCore {
else if (Object.keys(this._query.insert).length)
fromRaw = 'INTO';
fromRaw += this._query.from ? ` ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : '';
fromRaw += this._query.from ? ` ${this._query.schema ? `"${this._query.schema}".` : ''}"${this._query.from}" ` : '';
// WHERE
const whereArray = this._query.where.reduce(this._reducer, []);
@@ -1139,7 +1371,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}`;
}
/**
@@ -1156,15 +1391,23 @@ export class PostgreSQLClient extends AntaresCore {
nest: false,
details: false,
split: true,
comments: true,
...args
};
if (args.schema && args.schema !== 'public')
await this.use(args.schema);
if (!args.comments)
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
const resultsArr = [];
let paramsArr = [];
const queries = args.split ? sql.split(/(?!\B'[^']*);(?![^']*'\B)/gm) : [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

162
src/main/main.js Normal file
View File

@@ -0,0 +1,162 @@
'use strict';
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
import * as path from 'path';
import Store from 'electron-store';
import * as remoteMain from '@electron/remote/main';
import ipcHandlers from './ipc-handlers';
// remoteMain.initialize();
Store.initRenderer();
const isDevelopment = process.env.NODE_ENV !== 'production';
const isMacOS = process.platform === 'darwin';
const gotTheLock = app.requestSingleInstanceLock();
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
// global reference to mainWindow (necessary to prevent window from being garbage collected)
let mainWindow;
async function createMainWindow () {
const icon = require('../renderer/images/logo-32.png');
const window = new BrowserWindow({
width: 1024,
height: 800,
minWidth: 900,
minHeight: 550,
title: 'Antares SQL',
autoHideMenuBar: true,
icon: nativeImage.createFromDataURL(icon.default),
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
'web-security': false,
spellcheck: false
},
frame: false,
titleBarStyle: isMacOS ? 'hidden' : 'default',
trafficLightPosition: isMacOS ? { x: 10, y: 8 } : undefined,
backgroundColor: '#1d1d1d'
});
remoteMain.enable(window.webContents);
try {
if (isDevelopment) {
const { default: installExtension, VUEJS3_DEVTOOLS } = require('electron-devtools-installer');
const options = {
loadExtensionOptions: { allowFileAccess: true }
};
try {
const name = await installExtension(VUEJS3_DEVTOOLS, options);
console.log(`Added Extension: ${name}`);
}
catch (err) {
console.log('An error occurred: ', err);
}
await window.loadURL('http://localhost:9080');
}
else {
const indexPath = path.resolve(__dirname, 'index.html');
await window.loadFile(indexPath);
}
}
catch (err) {
console.log(err);
}
window.on('closed', () => {
mainWindow = null;
});
window.webContents.on('devtools-opened', () => {
window.focus();
setImmediate(() => {
window.focus();
});
});
return window;
}
if (!gotTheLock) app.quit();
else {
require('@electron/remote/main').initialize();
// Initialize ipcHandlers
ipcHandlers();
// quit application when all windows are closed
app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits
if (isMacOS) app.quit();
});
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 = await createMainWindow();
});
// create main BrowserWindow when electron is ready
app.on('ready', async () => {
mainWindow = await createMainWindow();
createAppMenu();
if (isDevelopment)
mainWindow.webContents.openDevTools();
process.on('uncaughtException', (error) => {
mainWindow.webContents.send('unhandled-exception', error);
});
process.on('unhandledRejection', (error) => {
mainWindow.webContents.send('unhandled-exception', error);
});
});
}
function createAppMenu () {
let menu = null;
if (isMacOS) {
menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{
label: 'Check for Updates...',
click: (_menuItem, win) => win.webContents.send('open-updates-preferences')
},
{
label: 'Preferences',
click: (_menuItem, win) => win.webContents.send('toggle-preferences'),
accelerator: 'CmdOrCtrl+,'
},
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ type: 'separator' },
{ role: 'quit' }
]
},
{
role: 'editMenu'
},
{
role: 'viewMenu'
},
{
role: 'windowMenu'
}
]);
}
Menu.setApplicationMenu(menu);
}

View File

@@ -4,21 +4,20 @@
<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" />
<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>
@@ -35,21 +34,19 @@ 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')
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',

View File

@@ -1,5 +1,5 @@
<template>
<div class="context">
<div class="context" :class="{'bottom': isBottom}">
<a
class="context-overlay"
@click="close"
@@ -23,21 +23,29 @@ export default {
},
data () {
return {
contextSize: null
contextSize: null,
isBottom: false
};
},
computed: {
position () {
const { clientY, clientX } = this.contextEvent;
let topCord = `${clientY + 5}px`;
let leftCord = `${clientX + 5}px`;
let topCord = 0;
let leftCord = 0;
if (this.contextSize) {
if (clientY + this.contextSize.height + 5 >= window.innerHeight)
topCord = `${clientY - this.contextSize.height}px`;
if (this.contextEvent) {
const { clientY, clientX } = this.contextEvent;
topCord = `${clientY + 2}px`;
leftCord = `${clientX + 5}px`;
if (clientX + this.contextSize.width + 5 >= window.innerWidth)
leftCord = `${clientX - this.contextSize.width}px`;
if (this.contextSize) {
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`;
}
}
return {
@@ -50,7 +58,8 @@ export default {
window.addEventListener('keydown', this.onKey);
},
mounted () {
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
if (this.$refs.contextContent)
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
@@ -83,12 +92,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 +115,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({

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;
@@ -78,6 +78,7 @@ export default {
.file-uploader-message {
display: flex;
border-radius: $border-radius 0 0 $border-radius;
}
.file-uploader-input {

View File

@@ -89,7 +89,7 @@
:type="inputProps().type"
:disabled="!isChecked"
>
<template v-if="methodData && 'params' in methodData" class="columns">
<template v-if="methodData && 'params' in methodData">
<input
v-for="(option, key) in methodData.params"
:key="key"

View File

@@ -41,7 +41,7 @@
</form>
</div>
</div>
<div class="modal-footer text-light">
<div class="modal-footer">
<button class="btn btn-primary mr-2" @click.stop="sendCredentials">
{{ $t('word.send') }}
</button>

View File

@@ -8,7 +8,8 @@
>
<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">
@@ -16,13 +17,13 @@
<form class="form-horizontal">
<div
v-for="(parameter, i) in inParameters"
:key="parameter._id"
:key="parameter._antares_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' : ''"
@@ -30,7 +31,11 @@
class="form-input"
type="text"
>
<span class="input-group-addon field-type" :class="typeClass(parameter.type)">
<span
:title="`${parameter.type} ${parameter.length}`"
class="input-group-addon field-type cut-text"
:class="typeClass(parameter.type)"
>
{{ parameter.type }} {{ parameter.length | wrapNumber }}
</span>
</div>
@@ -126,4 +131,8 @@ export default {
.field-type {
font-size: 0.6rem;
}
.input-group-addon {
max-width: 100px;
}
</style>

View File

@@ -2,8 +2,8 @@
<ConfirmModal
:confirm-text="$t('word.discard')"
:cancel-text="$t('word.stay')"
@confirm="discardUnsavedChanges"
@hide="closeUnsavedChangesModal"
@confirm="$emit('confirm')"
@hide="$emit('close')"
>
<template slot="header">
<div class="d-flex">
@@ -19,7 +19,6 @@
</template>
<script>
import { mapActions } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
@@ -34,13 +33,6 @@ export default {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
discardUnsavedChanges: 'workspaces/discardUnsavedChanges',
closeUnsavedChangesModal: 'workspaces/closeUnsavedChangesModal'
}),
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')

View File

@@ -1,391 +0,0 @@
<template>
<div class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-server mr-1" /> {{ $t('message.editConnection') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
<div class="modal-body p-0">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="c-hand">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="c-hand">{{ $t('word.ssl') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="localConnection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
</div>
<div class="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>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select>
</div>
</div>
<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.host"
class="form-input"
type="text"
>
</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.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.database"
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.user"
class="form-input"
type="text"
:disabled="localConnection.ask"
>
</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.password"
class="form-input"
type="password"
:disabled="localConnection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12" />
<div class="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>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div v-if="selectedTab === 'ssl'" 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.enableSsl') }}
</label>
</div>
<div class="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="isTesting || !localConnection.ssl">
<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.key"
:message="$t('word.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
</div>
<div class="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">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
</div>
<div class="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">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="localConnection.ciphers"
class="form-input"
type="text"
>
</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"
:class="{'loading': isTesting}"
@click="startTest"
>
{{ $t('message.testConnection') }}
</button>
<button class="btn btn-primary mr-2" @click="saveEditConnection">
{{ $t('word.save') }}
</button>
<button class="btn btn-link" @click="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast';
import BaseUploadInput from '@/components/BaseUploadInput';
export default {
name: 'ModalEditConnection',
components: {
ModalAskCredentials,
BaseToast,
BaseUploadInput
},
props: {
connection: Object
},
data () {
return {
toast: {
status: '',
message: ''
},
isTesting: false,
isAsking: false,
localConnection: null,
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () {
this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
editConnection: 'connections/editConnection'
}),
async startTest () {
this.isTesting = true;
this.toast = {
status: '',
message: ''
};
if (this.localConnection.ask)
this.isAsking = true;
else {
try {
const res = await Connection.makeTest(this.localConnection);
if (res.status === 'error')
this.toast = { status: 'error', message: res.response.message };
else
this.toast = { status: 'success', message: this.$t('message.connectionSuccessfullyMade') };
}
catch (err) {
this.toast = { 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 {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.toast = { status: 'error', message: res.response.message };
else
this.toast = { status: 'success', message: this.$t('message.connectionSuccessfullyMade') };
}
catch (err) {
this.toast = { status: 'error', message: err.stack };
}
this.isTesting = false;
},
saveEditConnection () {
this.editConnection(this.localConnection);
this.closeModal();
},
closeAsking () {
this.isTesting = false;
this.isAsking = false;
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.localConnection.ssl = !this.localConnection.ssl;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.localConnection[name] = files[0].path;
},
pathClear (name) {
this.localConnection[name] = '';
}
}
};
</script>
<style scoped>
.modal-container {
position: absolute;
max-width: 450px;
top: 17.5vh;
}
</style>

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" />
@@ -52,7 +53,7 @@
</form>
</div>
</div>
<div class="modal-footer text-light">
<div class="modal-footer">
<button class="btn btn-primary mr-2" @click.stop="updateSchema">
{{ $t('word.update') }}
</button>
@@ -71,7 +72,7 @@ import Schema from '@/ipc-api/Schema';
export default {
name: 'ModalEditSchema',
props: {
selectedDatabase: String
selectedSchema: String
},
data () {
return {
@@ -98,7 +99,7 @@ export default {
async created () {
let actualCollation;
try {
const { status, response } = await Schema.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase });
const { status, response } = await Schema.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedSchema });
if (status === 'success')
actualCollation = response;
@@ -111,8 +112,8 @@ export default {
}
this.database = {
name: this.selectedDatabase,
prevName: this.selectedDatabase,
name: this.selectedSchema,
prevName: this.selectedSchema,
collation: actualCollation || this.defaultCollation,
prevCollation: actualCollation || this.defaultCollation
};

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" />
@@ -51,7 +52,7 @@
</form>
</div>
</div>
<div class="modal-footer text-light columns">
<div class="modal-footer columns">
<div class="column d-flex" :class="hasFakes ? 'col-4' : 'col-2'">
<div class="input-group tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<input
@@ -250,33 +251,32 @@ export default {
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = Number.isNaN(+field.default) ? null : +field.default;
if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = !field.default || Number.isNaN(+field.default.replaceAll('\'', '')) ? null : +field.default.replaceAll('\'', '');
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = field.default
? field.default.includes('\'')
? field.default.split('\'')[1]
: field.default
: '';
}
if ([...TIME, ...DATE].includes(field.type))
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;
}
if (field.enumValues)
else if (field.enumValues)
fieldDefault = field.enumValues.replaceAll('\'', '').split(',');
else
fieldDefault = field.default;
}
rowObj[field.name] = { value: fieldDefault };

View File

@@ -0,0 +1,283 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-history mr-1" />
<span class="cut-text">{{ $t('word.history') }}: {{ connectionName }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body p-0 workspace-query-results">
<div
v-if="history.length"
ref="searchForm"
class="form-group has-icon-right p-2 m-0"
>
<input
v-model="searchTerm"
class="form-input"
type="text"
:placeholder="$t('message.searchForQueries')"
>
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" />
<i
v-else
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-4"
@click="searchTerm = ''"
/>
</div>
<div
v-if="history.length"
ref="tableWrapper"
class="vscroll px-1 "
:style="{'height': resultsSize+'px'}"
>
<div ref="table">
<BaseVirtualScroll
ref="resultTable"
:items="filteredHistory"
:item-height="66"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<div
v-for="query in items"
:key="query.uid"
class="tile my-2"
tabindex="0"
>
<div class="tile-icon">
<i class="mdi mdi-code-tags pr-1" />
</div>
<div class="tile-content">
<div class="tile-title">
<code
class="cut-text"
:title="query.sql"
v-html="highlightWord(query.sql)"
/>
</div>
<div class="tile-bottom-content">
<small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small>
<div class="tile-history-buttons">
<button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)">
<i class="mdi mdi-open-in-app pr-1" /> {{ $t('word.select') }}
</button>
<button class="btn btn-link pl-1" @click="copyQuery(query.sql)">
<i class="mdi mdi-content-copy pr-1" /> {{ $t('word.copy') }}
</button>
<button class="btn btn-link pl-1" @click="deleteQuery(query)">
<i class="mdi mdi-delete-forever pr-1" /> {{ $t('word.delete') }}
</button>
</div>
</div>
</div>
</div>
</template>
</BaseVirtualScroll>
</div>
</div>
<div v-else class="empty">
<div class="empty-icon">
<i class="mdi mdi-history mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereIsNoQueriesYet') }}
</p>
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import { mapGetters, mapActions } from 'vuex';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
export default {
name: 'ModalHistory',
components: {
BaseVirtualScroll
},
props: {
connection: Object
},
data () {
return {
resultsSize: 1000,
isQuering: false,
scrollElement: null,
searchTermInterval: null,
searchTerm: '',
localSearchTerm: ''
};
},
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName',
getHistoryByWorkspace: 'history/getHistoryByWorkspace'
}),
connectionName () {
return this.getConnectionName(this.connection.uid);
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
},
filteredHistory () {
return this.history.filter(q => q.sql.toLowerCase().search(this.searchTerm.toLowerCase()) >= 0);
}
},
watch: {
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => {
this.localSearchTerm = this.searchTerm;
}, 200);
}
},
created () {
window.addEventListener('keydown', this.onKey, { capture: true });
},
updated () {
if (this.$refs.table)
this.refreshScroller();
if (this.$refs.tableWrapper)
this.scrollElement = this.$refs.tableWrapper;
},
mounted () {
this.resizeResults();
window.addEventListener('resize', this.resizeResults);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
deleteQueryFromHistory: 'history/deleteQueryFromHistory'
}),
copyQuery (sql) {
navigator.clipboard.writeText(sql);
},
deleteQuery (query) {
this.deleteQueryFromHistory({
workspace: this.connection.uid,
...query
});
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el)
this.resultsSize = el.offsetHeight - this.$refs.searchForm.offsetHeight;
this.$refs.resultTable.updateWindow();
}
},
formatDate (date) {
return moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date;
},
refreshScroller () {
this.resizeResults();
},
closeModal () {
this.$emit('close');
},
highlightWord (string) {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
}
else
return string;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.tile {
border-radius: $border-radius;
display: flex;
align-items: center;
&:hover,
&:focus {
.tile-content {
.tile-bottom-content {
.tile-history-buttons {
opacity: 1;
}
}
}
}
.tile-icon {
font-size: 1.2rem;
margin-left: 0.3rem;
width: 28px;
}
.tile-content {
padding: 0.3rem;
padding-left: 0.1rem;
max-width: calc(100% - 30px);
code {
max-width: 100%;
display: inline-block;
font-size: 100%;
// color: $primary-color;
opacity: 0.8;
font-weight: 600;
}
.tile-subtitle {
opacity: 0.8;
}
.tile-bottom-content {
display: flex;
justify-content: space-between;
.tile-history-buttons {
opacity: 0;
transition: opacity 0.2s;
button {
font-size: 0.7rem;
height: 1rem;
line-height: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
}
}
}
}
</style>

View File

@@ -1,413 +0,0 @@
<template>
<div class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container">
<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') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
<div class="modal-body p-0">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="c-hand">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="c-hand">{{ $t('word.ssl') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
</div>
<div class="col-8 col-sm-12">
<select
v-model="connection.client"
class="form-select"
@change="setDefaults"
>
<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">
<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.host"
class="form-input"
type="text"
>
</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.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.database"
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.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</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.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12" />
<div class="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>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div v-if="selectedTab === 'ssl'" 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.enableSsl') }}
</label>
</div>
<div class="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="isTesting || !connection.ssl">
<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.key"
:message="$t('word.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
</div>
<div class="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">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
</div>
<div class="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">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
</div>
</div>
<div class="modal-footer text-light">
<button
class="btn btn-gray mr-2"
:class="{'loading': isTesting}"
@click="startTest"
>
{{ $t('message.testConnection') }}
</button>
<button class="btn btn-primary mr-2" @click="saveNewConnection">
{{ $t('word.save') }}
</button>
<button class="btn btn-link" @click="closeModal">
{{ $t('word.close') }}
</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 BaseToast from '@/components/BaseToast';
import BaseUploadInput from '@/components/BaseUploadInput';
export default {
name: 'ModalNewConnection',
components: {
ModalAskCredentials,
BaseToast,
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: ''
},
toast: {
status: '',
message: ''
},
isTesting: false,
isAsking: false,
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () {
this.setDefaults();
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
closeModal: 'application/hideNewConnModal',
addConnection: 'connections/addConnection'
}),
setDefaults () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
},
async startTest () {
this.isTesting = true;
this.toast = {
status: '',
message: ''
};
if (this.connection.ask)
this.isAsking = true;
else {
try {
const res = await Connection.makeTest(this.connection);
if (res.status === 'error')
this.toast = { status: 'error', message: res.response.message };
else
this.toast = { status: 'success', message: this.$t('message.connectionSuccessfullyMade') };
}
catch (err) {
this.toast = { 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 {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.toast = { status: 'error', message: res.response.message };
else
this.toast = { status: 'success', message: this.$t('message.connectionSuccessfullyMade') };
}
catch (err) {
this.toast = { status: 'error', message: err.stack };
}
this.isTesting = false;
},
saveNewConnection () {
this.addConnection(this.connection);
this.closeModal();
},
closeAsking () {
this.isAsking = false;
this.isTesting = false;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.connection.ssl = !this.connection.ssl;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.connection[name] = files[0].path;
},
pathClear (name) {
this.connection[name] = '';
}
}
};
</script>
<style scoped>
.modal-container {
position: absolute;
max-width: 450px;
top: 17.5vh;
}
</style>

View File

@@ -1,170 +0,0 @@
<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.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>
<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>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="localFunction.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="localFunction.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewFunction',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localFunction: {
definer: '',
sql: '',
parameters: [],
name: '',
comment: '',
language: null,
returns: null,
returnsLength: 10,
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
},
isOptionsChanging: false
};
},
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.procedureSql)
this.localFunction.sql = this.customizations.procedureSql;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewFunction () {
this.$emit('open-create-function-editor', this.localFunction);
}
}
};
</script>

View File

@@ -1,168 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewRoutine"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewRoutine') }}
</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="localRoutine.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="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>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localRoutine.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="localRoutine.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="localRoutine.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="localRoutine.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewRoutine',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localRoutine: {
definer: '',
sql: '',
parameters: [],
name: '',
comment: '',
language: null,
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
},
isOptionsChanging: false
};
},
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);
},
methods: {
confirmNewRoutine () {
this.$emit('open-create-routine-editor', this.localRoutine);
}
}
};
</script>

View File

@@ -1,109 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewTrigger"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewScheduler') }}
</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="localScheduler.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localScheduler.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 class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localScheduler.comment"
class="form-input"
type="text"
>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewScheduler',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localScheduler: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
comment: '',
execution: 'EVERY',
every: ['1', 'DAY'],
preserve: true,
state: 'DISABLE'
}
};
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewTrigger () {
this.$emit('open-create-scheduler-editor', this.localScheduler);
}
}
};
</script>

View File

@@ -5,14 +5,15 @@
<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" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<form class="form-horizontal" @submit.prevent="createSchema">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}</label>
@@ -48,7 +49,7 @@
</form>
</div>
</div>
<div class="modal-footer text-light">
<div class="modal-footer">
<button
class="btn btn-primary mr-2"
:class="{'loading': isLoading}"

View File

@@ -1,130 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-plus mr-1" /> {{ $t('message.createNewTable') }}
</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="localOptions.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localOptions.comment"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4">
{{ $t('word.collation') }}
</label>
<div class="column">
<select v-model="localOptions.collation" class="form-select">
<option
v-for="collation in workspace.collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</div>
</div>
<div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4">
{{ $t('word.engine') }}
</label>
<div class="column">
<select v-model="localOptions.engine" class="form-select">
<option
v-for="engine in workspace.engines"
:key="engine.name"
:value="engine.name"
>
{{ engine.name }}
</option>
</select>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import { mapGetters } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewTable',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localOptions: {
name: '',
comment: '',
collation: '',
engine: ''
},
isOptionsChanging: false
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
defaultCollation () {
if (this.workspace.customizations.collations)
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
return '';
},
defaultEngine () {
if (this.workspace.customizations.engines)
return this.workspace.engines.find(engine => engine.isDefault).name;
return '';
}
},
mounted () {
this.localOptions.collation = this.defaultCollation;
this.localOptions.engine = this.defaultEngine;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
this.$emit('open-create-table-editor', this.localOptions);
}
}
};
</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-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" />
@@ -85,7 +86,7 @@
</form>
</div>
</div>
<div class="modal-footer text-light">
<div class="modal-footer">
<div class="input-group col-3 tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<input
v-model="nInserts"

View File

@@ -1,140 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewTrigger"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewTrigger') }}
</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="localTrigger.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localTrigger.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 class="form-group">
<label class="form-label col-4">
{{ $t('word.table') }}
</label>
<div class="column">
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.event') }}
</label>
<div class="column">
<div class="input-group">
<select v-model="localTrigger.event1" 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>
</div>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewTrigger',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localTrigger: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
table: '',
event1: 'BEFORE',
event2: 'INSERT'
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
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') : [];
}
},
mounted () {
this.localTrigger.table = this.schemaTables.length ? this.schemaTables[0].name : '';
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewTrigger () {
this.$emit('open-create-trigger-editor', this.localTrigger);
}
}
};
</script>

View File

@@ -1,192 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmOptionsChange"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-eye-plus mr-1" /> {{ $t('message.createNewView') }}
</div>
</template>
<div :slot="'body'">
<div class="container">
<div class="columns mb-4">
<div class="column col-6">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-6">
<div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select v-model="localView.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>
</div>
</div>
</div>
<div class="columns">
<div class="column col-4">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="DEFINER"
>
<i class="form-icon" /> DEFINER
</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="INVOKER"
>
<i class="form-icon" /> INVOKER
</label>
</div>
</div>
<div class="column col-4">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
name="algorithm"
value="UNDEFINED"
>
<i class="form-icon" /> UNDEFINED
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="MERGE"
name="algorithm"
>
<i class="form-icon" /> MERGE
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="TEMPTABLE"
name="algorithm"
>
<i class="form-icon" /> TEMPTABLE
</label>
</div>
</div>
<div class="column col-4">
<div v-if="workspace.customizations.viewUpdateOption" class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value=""
>
<i class="form-icon" /> None
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="CASCADED"
>
<i class="form-icon" /> CASCADED
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="LOCAL"
>
<i class="form-icon" /> LOCAL
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label>
<QueryEditor
ref="queryEditor"
:value.sync="localView.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: 'ModalNewView',
components: {
ConfirmModal,
QueryEditor
},
props: {
workspace: Object
},
data () {
return {
localView: {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
},
isOptionsChanging: false,
editorHeight: 300
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
}
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
this.$emit('open-create-view-editor', this.localView);
}
}
};
</script>

View File

@@ -1,44 +1,75 @@
<template>
<div class="modal active">
<ModalProcessesListContext
v-if="isContext"
:context-event="contextEvent"
:selected-row="selectedRow"
:selected-cell="selectedCell"
@copy-cell="copyCell"
@copy-row="copyRow"
@kill-process="killProcess"
@close-context="closeContext"
/>
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<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" />
</div>
<div class="processes-toolbar py-2 px-4">
<div class="dropdown">
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
:class="{'loading':isQuering}"
title="F5"
@click="getProcessesList"
>
<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" />
</div>
<div class="menu px-3">
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
<input
v-model="autorefreshTimer"
class="slider no-border"
type="range"
min="0"
max="15"
step="0.5"
@change="setRefreshInterval"
<div class="workspace-query-buttons">
<div class="dropdown pr-1">
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
:class="{'loading':isQuering}"
:title="`${$t('word.refresh')} (F5)`"
@click="getProcessesList"
>
<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" />
</button>
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
<i class="mdi mdi-24px mdi-menu-down" />
</div>
<div class="menu px-3">
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
<input
v-model="autorefreshTimer"
class="slider no-border"
type="range"
min="0"
max="15"
step="0.5"
@change="setRefreshInterval"
>
</div>
</div>
</div>
<div class="dropdown table-dropdown">
<button
:disabled="isQuering"
class="btn btn-dark btn-sm dropdown-toggle d-flex 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 v-if="sortedResults.length">
@@ -82,11 +113,12 @@
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<ProcessesListRow
<ModalProcessesListRow
v-for="row in items"
:key="row._id"
:key="row.id"
class="process-row"
:row="row"
@select-row="selectRow(row.id)"
@contextmenu="contextMenu"
@stop-refresh="stopRefresh"
/>
@@ -101,15 +133,18 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import arrayToFile from '../libs/arrayToFile';
import Schema from '@/ipc-api/Schema';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ProcessesListRow from '@/components/ProcessesListRow';
import ModalProcessesListRow from '@/components/ModalProcessesListRow';
import ModalProcessesListContext from '@/components/ModalProcessesListContext';
export default {
name: 'ModalProcessesList',
components: {
BaseVirtualScroll,
ProcessesListRow
ModalProcessesListRow,
ModalProcessesListContext
},
props: {
connection: Object
@@ -118,8 +153,12 @@ export default {
return {
resultsSize: 1000,
isQuering: false,
isContext: false,
autorefreshTimer: 0,
refreshInterval: null,
contextEvent: null,
selectedCell: null,
selectedRow: null,
results: [],
fields: [],
currentSort: '',
@@ -244,10 +283,55 @@ export default {
this.autorefreshTimer = 0;
this.clearRefresh();
},
contextMenu () {},
selectRow (row) {
this.selectedRow = row;
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.stopRefresh();
this.selectedCell = cell;
this.selectedRow = cell.id;
this.contextEvent = event;
this.isContext = true;
},
async killProcess () {
try { // Table data
const { status, response } = await Schema.killProcess({ uid: this.connection.uid, pid: this.selectedRow });
if (status === 'success')
this.getProcessesList();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
closeContext () {
this.isContext = false;
},
copyCell () {
const row = this.results.find(row => row.id === this.selectedRow);
const valueToCopy = row[this.selectedCell.field];
navigator.clipboard.writeText(valueToCopy);
},
copyRow () {
const row = this.results.find(row => row.id === this.selectedRow);
const rowToCopy = JSON.parse(JSON.stringify(row));
navigator.clipboard.writeText(JSON.stringify(rowToCopy));
},
closeModal () {
this.$emit('close');
},
downloadTable (format) {
if (!this.sortedResults) return;
arrayToFile({
type: format,
content: this.sortedResults,
filename: 'processes'
});
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')

View File

@@ -0,0 +1,75 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<div v-if="selectedRow" 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="selectedRow"
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="selectedRow"
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="selectedRow"
class="context-element"
@click="killProcess"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-close-circle-outline text-light pr-1" /> {{ $t('message.killProcess') }}
</span>
</div>
</BaseContextMenu>
</template>
<script>
import BaseContextMenu from '@/components/BaseContextMenu';
export default {
name: 'ModalProcessesListContext',
components: {
BaseContextMenu
},
props: {
contextEvent: MouseEvent,
selectedRow: Number,
selectedCell: Object
},
computed: {
},
methods: {
closeContext () {
this.$emit('close-context');
},
copyCell () {
this.$emit('copy-cell');
this.closeContext();
},
copyRow () {
this.$emit('copy-row');
this.closeContext();
},
killProcess () {
this.$emit('kill-process');
this.closeContext();
}
}
};
</script>

View File

@@ -1,21 +1,18 @@
<template>
<div class="tr" @click="selectRow($event, row._id)">
<div class="tr" @click="selectRow()">
<div
v-for="(col, cKey) in row"
v-show="cKey !== '_id'"
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
@contextmenu.prevent="openContext($event, { id: row.id, field: cKey })"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
</template>
<span
v-if="!isInlineEditor[cKey]"
class="cell-content"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
</div>
<ConfirmModal
v-if="isInfoModal"
@@ -51,7 +48,7 @@ import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
export default {
name: 'ProcessesListRow',
name: 'ModalProcessesListRow',
components: {
ConfirmModal,
TextEditor
@@ -73,25 +70,15 @@ export default {
};
},
computed: {},
watch: {
fields () {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
},
methods: {
isNull (value) {
return value === null ? ' is-null' : '';
},
selectRow (event, row) {
this.$emit('select-row', event, row);
selectRow () {
this.$emit('select-row');
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
this.$emit('contextmenu', event, payload);
},
hideInfoModal () {
this.isInfoModal = false;
@@ -126,6 +113,7 @@ export default {
.cell-content {
display: block;
padding: 0 0.2rem;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;

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,40 +16,40 @@
<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="c-hand">{{ $t('word.changelog') }}</a>
<a class="tab-link">{{ $t('word.changelog') }}</a>
</li>
<li
class="tab-item"
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>
@@ -59,15 +59,15 @@
<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="col-6 col-sm-12">
<div class="column col-9 col-sm-12 mb-2">
<div class="form-group">
<div class="col-7 col-sm-12">
<label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<select
v-model="localLocale"
class="form-select"
@@ -84,12 +84,46 @@
</div>
</div>
<div class="form-group">
<div class="col-6 col-sm-12">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.dataTabPageSize') }}
</label>
</div>
<div class="col-5 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 mb-0">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.restorePreviourSession') }}
</label>
</div>
<div class="col-5 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleRestoreSession">
<input type="checkbox" :checked="restoreTabs">
<i class="form-icon" />
</label>
</div>
</div>
<div class="form-group">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.notificationsTimeout') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<div class="input-group">
<input
v-model="localTimeout"
@@ -107,29 +141,27 @@
<div class="column col-12 h6 mt-4 text-uppercase mb-1">
{{ $t('word.editor') }}
</div>
<div class="column col-8 col-sm-12">
<div class="form-group">
<div class="col-6 col-sm-12">
<div class="column col-9 col-sm-12">
<div class="form-group mb-0">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('word.autoCompletion') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleAutoComplete">
<input type="checkbox" :checked="selectedAutoComplete">
<i class="form-icon" />
</label>
</div>
</div>
</div>
<div class="column col-8 col-sm-12">
<div class="form-group">
<div class="col-6 col-sm-12">
<div class="form-group mb-0">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.wrapLongLines') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleLineWrap">
<input type="checkbox" :checked="selectedLineWrap">
<i class="form-icon" />
@@ -152,7 +184,7 @@
:class="{'selected': applicationTheme === 'dark'}"
@click="changeApplicationTheme('dark')"
>
<img :src="require('@/images/dark.png').default" class="img-responsive img-fit-cover s-rounded">
<img src="../images/dark.png" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-light">
<i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4">
@@ -165,7 +197,7 @@
:class="{'selected': applicationTheme === 'light'}"
@click="changeApplicationTheme('light')"
>
<img :src="require('@/images/light.png').default" class="img-responsive img-fit-cover s-rounded">
<img src="../images/light.png" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-dark">
<i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4">
@@ -201,6 +233,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"
@@ -223,7 +280,7 @@
<div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center">
<img :src="require('@/images/logo.svg').default" width="128">
<img src="../images/logo.svg" width="128">
<h4>{{ appName }}</h4>
<p>
{{ $t('word.version') }} {{ appVersion }}<br>
@@ -257,9 +314,11 @@ export default {
data () {
return {
localLocale: null,
localPageSize: null,
localTimeout: null,
localEditorTheme: null,
selectedTab: 'general',
pageSizes: [30, 40, 100, 250, 500, 1000],
editorThemes: [
{
group: this.$t('word.light'),
@@ -317,11 +376,14 @@ export default {
appVersion: 'application/appVersion',
selectedSettingTab: 'application/selectedSettingTab',
selectedLocale: 'settings/getLocale',
pageSize: 'settings/getDataTabLimit',
selectedAutoComplete: 'settings/getAutoComplete',
selectedLineWrap: 'settings/getLineWrap',
notificationsTimeout: 'settings/getNotificationsTimeout',
restoreTabs: 'settings/getRestoreTabs',
applicationTheme: 'settings/getApplicationTheme',
editorTheme: 'settings/getEditorTheme',
editorFontSize: 'settings/getEditorFontSize',
updateStatus: 'application/getUpdateStatus',
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
@@ -334,7 +396,7 @@ export default {
return locales;
},
hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
},
workspace () {
return this.getWorkspace(this.selectedWorkspace);
@@ -359,6 +421,7 @@ ORDER BY
},
created () {
this.localLocale = this.selectedLocale;
this.localPageSize = this.pageSize;
this.localTimeout = this.notificationsTimeout;
this.localEditorTheme = this.editorTheme;
this.selectedTab = this.selectedSettingTab;
@@ -371,10 +434,13 @@ ORDER BY
...mapActions({
closeModal: 'application/hideSettingModal',
changeLocale: 'settings/changeLocale',
changePageSize: 'settings/changePageSize',
changeRestoreTabs: 'settings/changeRestoreTabs',
changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap',
changeApplicationTheme: 'settings/changeApplicationTheme',
changeEditorTheme: 'settings/changeEditorTheme',
changeEditorFontSize: 'settings/changeEditorFontSize',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}),
selectTab (tab) {
@@ -394,6 +460,9 @@ ORDER BY
if (e.key === 'Escape')
this.closeModal();
},
toggleRestoreSession () {
this.changeRestoreTabs(!this.restoreTabs);
},
toggleAutoComplete () {
this.changeAutoComplete(!this.selectedAutoComplete);
},

View File

@@ -16,7 +16,7 @@
<script>
import { mapGetters } from 'vuex';
import marked from 'marked';
import { marked } from 'marked';
import BaseLoader from '@/components/BaseLoader';
export default {

View File

@@ -29,12 +29,19 @@
{{ $t('message.checkForUpdates') }}
</button>
<button
v-if="updateStatus === 'downloaded'"
v-else-if="updateStatus === 'downloaded'"
class="btn btn-primary"
@click="restartToUpdate"
>
{{ $t('message.restartToInstall') }}
</button>
<button
v-else-if="updateStatus === 'link'"
class="btn btn-primary"
@click="openOutside('https://antares-sql.app/download.html')"
>
{{ $t('message.goToDownloadPage') }}
</button>
</div>
<div class="form-group mt-4">
<label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease">
@@ -47,7 +54,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import { ipcRenderer } from 'electron';
import { ipcRenderer, shell } from 'electron';
export default {
name: 'ModalSettingsUpdate',
@@ -71,6 +78,8 @@ export default {
return this.$t('message.downloadingUpdate');
case 'downloaded':
return this.$t('message.updateDownloaded');
case 'link':
return this.$t('message.updateAvailable');
default:
return this.updateStatus;
}
@@ -80,6 +89,9 @@ export default {
...mapActions({
changeAllowPrerelease: 'settings/changeAllowPrerelease'
}),
openOutside (link) {
shell.openExternal(link);
},
checkForUpdates () {
ipcRenderer.send('check-for-updates');
},

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'
@@ -158,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({
@@ -173,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) {

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

@@ -1,37 +0,0 @@
<template>
<div class="columns">
<div class="column col-12 empty text-light">
<div class="empty-icon">
<i class="mdi mdi-48px mdi-emoticon" />
</div>
<p class="empty-title h5">
{{ $t('message.appWelcome') }}
</p>
<p class="empty-subtitle">
{{ $t('message.appFirstStep') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="$emit('new-conn')">
{{ $t('message.createConnection') }}
</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TheAppWelcome'
};
</script>
<style scoped>
.empty {
height: 100%;
border-radius: 0;
background: transparent;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>

View File

@@ -8,24 +8,29 @@
@close-context="isContext = false"
/>
<ul class="settingbar-elements">
<draggable v-model="connections">
<Draggable
v-model="connections"
@start="isDragging = true"
@end="dragStop"
>
<li
v-for="connection in connections"
:key="connection.uid"
draggable="true"
class="settingbar-element btn btn-link ex-tooltip"
:class="{'selected': connection.uid === selectedWorkspace}"
@click="selectWorkspace(connection.uid)"
@click.stop="selectWorkspace(connection.uid)"
@contextmenu.prevent="contextMenu($event, connection)"
@mouseover.self="tooltipPosition"
>
<i class="settingbar-element-icon dbi" :class="`dbi-${connection.client} ${getStatusBadge(connection.uid)}`" />
<span class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span>
<span v-if="!isDragging" class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span>
</li>
</draggable>
</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" />
@@ -51,19 +56,20 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import draggable from 'vuedraggable';
import Draggable from 'vuedraggable';
import SettingBarContext from '@/components/SettingBarContext';
export default {
name: 'TheSettingBar',
components: {
draggable,
Draggable,
SettingBarContext
},
data () {
return {
dragElement: null,
isContext: false,
isDragging: false,
contextEvent: null,
contextConnection: {},
scale: 0
@@ -86,13 +92,12 @@ export default {
}
},
hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
}
},
methods: {
...mapActions({
updateConnections: 'connections/updateConnections',
showNewConnModal: 'application/showNewConnModal',
showSettingModal: 'application/showSettingModal',
showScratchpad: 'application/showScratchpad',
selectWorkspace: 'workspaces/selectWorkspace'
@@ -106,13 +111,13 @@ export default {
return connection.ask ? '' : `${connection.user + '@'}${connection.host}:${connection.port}`;
},
tooltipPosition (e) {
const el = e.target;
const el = e.target ? e.target : e;
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;
const status = this.getWorkspace(uid).connectionStatus;
switch (status) {
case 'connected':
@@ -125,6 +130,13 @@ export default {
return '';
}
}
},
dragStop (e) {
this.isDragging = false;
setTimeout(() => {
this.tooltipPosition(e.originalEvent.target.parentNode);
}, 200);
}
}
};
@@ -167,13 +179,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;
@@ -181,9 +193,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;
@@ -211,7 +238,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;
@@ -220,7 +247,13 @@ export default {
transition: opacity 0.2s;
}
&:hover .ex-tooltip-content {
&.sortable-chosen {
.ex-tooltip-content {
opacity: 0 !important;
}
}
&:hover:not(.selected) .ex-tooltip-content {
visibility: visible;
opacity: 1;
}

View File

@@ -1,8 +1,12 @@
<template>
<div id="titlebar">
<div id="titlebar" @dblclick="toggleFullScreen">
<div class="titlebar-resizer" />
<div class="titlebar-elements">
<img class="titlebar-logo" :src="require('@/images/logo.svg').default">
<img
v-if="!isMacOS"
class="titlebar-logo"
src="@/images/logo.svg"
>
</div>
<div class="titlebar-elements titlebar-title">
{{ windowTitle }}
@@ -22,14 +26,26 @@
>
<i class="mdi mdi-24px mdi-refresh" />
</div>
<div class="titlebar-element" @click="minimizeApp">
<div
v-if="!isMacOS"
class="titlebar-element"
@click="minimizeApp"
>
<i class="mdi mdi-24px mdi-minus" />
</div>
<div class="titlebar-element" @click="toggleFullScreen">
<div
v-if="!isMacOS"
class="titlebar-element"
@click="toggleFullScreen"
>
<i v-if="isMaximized" class="mdi mdi-24px mdi-fullscreen-exit" />
<i v-else class="mdi mdi-24px mdi-fullscreen" />
</div>
<div class="titlebar-element close-button" @click="closeApp">
<div
v-if="!isMacOS"
class="titlebar-element close-button"
@click="closeApp"
>
<i class="mdi mdi-24px mdi-close" />
</div>
</div>
@@ -47,7 +63,8 @@ export default {
return {
w: getCurrentWindow(),
isMaximized: getCurrentWindow().isMaximized(),
isDevelopment: process.env.NODE_ENV === 'development'
isDevelopment: process.env.NODE_ENV === 'development',
isMacOS: process.platform === 'darwin'
};
},
computed: {
@@ -58,6 +75,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);

View File

@@ -1,13 +1,262 @@
<template>
<div v-show="isSelected" class="workspace column columns col-gapless">
<WorkspaceExploreBar :connection="connection" :is-selected="isSelected" />
<div v-if="workspace.connection_status === 'connected'" class="workspace-tabs column columns col-gapless">
<ul
id="tabWrap"
<WorkspaceExploreBar
v-if="workspace.connectionStatus === 'connected'"
:connection="connection"
:is-selected="isSelected"
/>
<div v-if="workspace.connectionStatus === 'connected'" class="workspace-tabs column columns col-gapless">
<Draggable
ref="tabWrap"
v-model="draggableTabs"
tag="ul"
group="tabs"
class="tab tab-block column col-12"
draggable=".tab-draggable"
@mouseover.native="addWheelEvent"
>
<li class="tab-item dropdown tools-dropdown">
<li
v-for="(tab, i) of draggableTabs"
:key="i"
:ref="selectedTab === tab.uid ? 'tab-selected' : ''"
class="tab-item tab-draggable"
draggable="true"
:class="{'active': selectedTab === tab.uid}"
@mousedown.left="selectTab({uid: workspace.uid, tab: tab.uid})"
@mouseup.middle="closeTab(tab)"
>
<a v-if="tab.type === 'query'" class="tab-link">
<i class="mdi mdi-18px mdi-code-tags mr-1" />
<span>
<span>{{ tab.content || 'Query' | cutText }} #{{ tab.index }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'temp-data'"
class="tab-link"
@dblclick="openAsPermanentTab(tab)"
>
<i class="mdi mdi-18px mr-1" :class="tab.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
<span class=" text-italic">{{ tab.elementName | cutText }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a v-else-if="tab.type === 'data'" class="tab-link">
<i class="mdi mdi-18px mr-1" :class="tab.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ tab.elementName | cutText }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-table'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newTable') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'table-props'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ tab.elementName | cutText }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'view-props'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.view`)}`">
{{ tab.elementName | cutText }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-view'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newView') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-trigger'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newTrigger') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-routine'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newRoutine') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-function'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newFunction') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-trigger-function'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newTriggerFunction') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-scheduler'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newScheduler') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type.includes('temp-')"
class="tab-link"
:class="{'badge': tab.isChanged}"
@dblclick="openAsPermanentTab(tab)"
>
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
<span class=" text-italic">{{ tab.elementName | cutText }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ tab.elementName | cutText }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
</li>
<li slot="header" class="tab-item dropdown tools-dropdown">
<a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
@@ -44,143 +293,230 @@
</li>
</ul>
</li>
<li
v-if="schemaChild && isSettingSupported"
class="tab-item"
:class="{'active': selectedTab === 'prop'}"
@click="selectTab({uid: workspace.uid, tab: 'prop'})"
>
<a class="tab-link">
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="schemaChild">{{ $t('word.settings').toUpperCase() }}: {{ schemaChild }}</span>
</a>
</li>
<li
v-if="workspace.breadcrumbs.table || workspace.breadcrumbs.view"
class="tab-item"
:class="{'active': selectedTab === 'data'}"
@click="selectTab({uid: workspace.uid, tab: 'data'})"
>
<a class="tab-link">
<i class="mdi mdi-18px mr-1" :class="workspace.breadcrumbs.table ? 'mdi-table' : 'mdi-table-eye'" />
<span :title="schemaChild">{{ $t('word.data').toUpperCase() }}: {{ schemaChild }}</span>
</a>
</li>
<li
v-for="tab of queryTabs"
:key="tab.uid"
class="tab-item"
:class="{'active': selectedTab === tab.uid}"
@click="selectTab({uid: workspace.uid, tab: tab.uid})"
@mouseup.middle="closeTab(tab.uid)"
>
<a class="tab-link">
<i class="mdi mdi-18px mdi-code-tags mr-1" />
<span>
Query #{{ tab.index }}
<span
v-if="queryTabs.length > 1"
class="btn btn-clear"
:title="$t('word.close')"
@click.stop="closeTab(tab.uid)"
/>
</span>
</a>
</li>
<li class="tab-item">
<li slot="footer" class="tab-item">
<a
class="tab-add"
:title="$t('message.openNewTab')"
@click="addTab"
@click="addQueryTab"
>
<i class="mdi mdi-24px mdi-plus" />
</a>
</li>
</ul>
<WorkspacePropsTab
v-show="selectedTab === 'prop' && workspace.breadcrumbs.table"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:table="workspace.breadcrumbs.table"
/>
<WorkspacePropsTabView
v-show="selectedTab === 'prop' && workspace.breadcrumbs.view"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:view="workspace.breadcrumbs.view"
/>
<WorkspacePropsTabTrigger
v-show="selectedTab === 'prop' && workspace.breadcrumbs.trigger"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:trigger="workspace.breadcrumbs.trigger"
/>
<WorkspacePropsTabRoutine
v-show="selectedTab === 'prop' && workspace.breadcrumbs.procedure"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:routine="workspace.breadcrumbs.procedure"
/>
<WorkspacePropsTabFunction
v-show="selectedTab === 'prop' && workspace.breadcrumbs.function"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:function="workspace.breadcrumbs.function"
/>
<WorkspacePropsTabScheduler
v-show="selectedTab === 'prop' && workspace.breadcrumbs.scheduler"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:scheduler="workspace.breadcrumbs.scheduler"
/>
<WorkspaceTableTab
v-show="selectedTab === 'data'"
:connection="connection"
:table="workspace.breadcrumbs.table || workspace.breadcrumbs.view"
/>
<WorkspaceQueryTab
v-for="tab of queryTabs"
:key="tab.uid"
:tab="tab"
:is-selected="selectedTab === tab.uid"
:connection="connection"
/>
</Draggable>
<WorkspaceEmptyState v-if="!workspace.tabs.length" @new-tab="addQueryTab" />
<template v-for="tab of workspace.tabs">
<WorkspaceTabQuery
v-if="tab.type==='query'"
:key="tab.uid"
:tab="tab"
:is-selected="selectedTab === tab.uid"
:connection="connection"
/>
<WorkspaceTabTable
v-else-if="['temp-data', 'data'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:table="tab.elementName"
:schema="tab.schema"
:element-type="tab.elementType"
/>
<WorkspaceTabNewTable
v-else-if="tab.type === 'new-table'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:schema="tab.schema"
/>
<WorkspaceTabPropsTable
v-else-if="tab.type === 'table-props'"
:key="tab.uid"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:table="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabNewView
v-else-if="tab.type === 'new-view'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:schema="tab.schema"
/>
<WorkspaceTabPropsView
v-else-if="tab.type === 'view-props'"
:key="tab.uid"
:is-selected="selectedTab === tab.uid"
:connection="connection"
:view="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabNewTrigger
v-else-if="tab.type === 'new-trigger'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsTrigger
v-else-if="['temp-trigger-props', 'trigger-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabNewTriggerFunction
v-else-if="tab.type === 'new-trigger-function'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsTriggerFunction
v-else-if="['temp-trigger-function-props', 'trigger-function-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:function="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabNewRoutine
v-else-if="tab.type === 'new-routine'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsRoutine
v-else-if="['temp-routine-props', 'routine-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:routine="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabNewFunction
v-else-if="tab.type === 'new-function'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsFunction
v-else-if="['temp-function-props', 'function-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:function="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabNewScheduler
v-else-if="tab.type === 'new-scheduler'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsScheduler
v-else-if="['temp-scheduler-props', 'scheduler-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:scheduler="tab.elementName"
:schema="tab.schema"
/>
</template>
</div>
<WorkspaceEditConnectionPanel v-else :connection="connection" />
<ModalProcessesList
v-if="isProcessesModal"
:connection="connection"
@close="hideProcessesModal"
/>
<ModalDiscardChanges
v-if="unsavedTab"
@confirm="closeTab(unsavedTab, true)"
@close="unsavedTab = null"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import Draggable from 'vuedraggable';
import Connection from '@/ipc-api/Connection';
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
import WorkspaceQueryTab from '@/components/WorkspaceQueryTab';
import WorkspaceTableTab from '@/components/WorkspaceTableTab';
import WorkspacePropsTab from '@/components/WorkspacePropsTab';
import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel';
import WorkspaceTabQuery from '@/components/WorkspaceTabQuery';
import WorkspaceTabTable from '@/components/WorkspaceTabTable';
import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable';
import WorkspaceTabNewView from '@/components/WorkspaceTabNewView';
import WorkspaceTabNewTrigger from '@/components/WorkspaceTabNewTrigger';
import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine';
import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction';
import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler';
import WorkspaceTabNewTriggerFunction from '@/components/WorkspaceTabNewTriggerFunction';
import WorkspaceTabPropsTable from '@/components/WorkspaceTabPropsTable';
import WorkspaceTabPropsView from '@/components/WorkspaceTabPropsView';
import WorkspaceTabPropsTrigger from '@/components/WorkspaceTabPropsTrigger';
import WorkspaceTabPropsTriggerFunction from '@/components/WorkspaceTabPropsTriggerFunction';
import WorkspaceTabPropsRoutine from '@/components/WorkspaceTabPropsRoutine';
import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction';
import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler';
import ModalProcessesList from '@/components/ModalProcessesList';
import ModalDiscardChanges from '@/components/ModalDiscardChanges';
export default {
name: 'Workspace',
components: {
Draggable,
WorkspaceEmptyState,
WorkspaceExploreBar,
WorkspaceQueryTab,
WorkspaceTableTab,
WorkspacePropsTab,
WorkspacePropsTabView,
WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabScheduler,
ModalProcessesList
WorkspaceEditConnectionPanel,
WorkspaceTabQuery,
WorkspaceTabTable,
WorkspaceTabNewTable,
WorkspaceTabPropsTable,
WorkspaceTabNewView,
WorkspaceTabPropsView,
WorkspaceTabNewTrigger,
WorkspaceTabPropsTrigger,
WorkspaceTabNewTriggerFunction,
WorkspaceTabPropsTriggerFunction,
WorkspaceTabNewRoutine,
WorkspaceTabNewFunction,
WorkspaceTabPropsRoutine,
WorkspaceTabPropsFunction,
WorkspaceTabNewScheduler,
WorkspaceTabPropsScheduler,
ModalProcessesList,
ModalDiscardChanges
},
filters: {
cutText (string) {
const limit = 20;
const escapedString = string.replace(/\s{2,}/g, ' ');
if (escapedString.length > limit)
return `${escapedString.substr(0, limit)}...`;
return escapedString;
}
},
props: {
connection: Object
@@ -188,7 +524,8 @@ export default {
data () {
return {
hasWheelEvent: false,
isProcessesModal: false
isProcessesModal: false,
unsavedTab: null
};
},
computed: {
@@ -199,6 +536,14 @@ export default {
workspace () {
return this.getWorkspace(this.connection.uid);
},
draggableTabs: {
get () {
return this.workspace.tabs;
},
set (val) {
this.updateTabs({ uid: this.connection.uid, tabs: val });
}
},
isSelected () {
return this.selectedWorkspace === this.connection.uid;
},
@@ -208,32 +553,12 @@ 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;
},
selectedTab () {
if (
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
) ||
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.selected_tab === 'data'
)
)
return this.queryTabs[0].uid;
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) ||
['data', 'prop'].includes(this.workspace.selected_tab)
? this.workspace.selected_tab
: this.queryTabs[0].uid;
return this.workspace ? this.workspace.selectedTab : null;
},
queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query');
@@ -246,19 +571,29 @@ export default {
return false;
}
},
watch: {
selectedTab (newVal, oldVal) {
if (newVal !== oldVal) {
setTimeout(() => {
const element = this.$refs['tab-selected'] ? this.$refs['tab-selected'][0] : null;
if (element) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
}, 50);
}
}
},
async created () {
window.addEventListener('keydown', this.onKey);
await this.addWorkspace(this.connection.uid);
const isInitiated = await Connection.checkConnection(this.connection.uid);
if (isInitiated)
this.connectWorkspace(this.connection);
},
mounted () {
if (this.$refs.tabWrap) {
this.$refs.tabWrap.addEventListener('wheel', e => {
if (e.deltaY > 0) this.$refs.tabWrap.scrollLeft += 50;
else this.$refs.tabWrap.scrollLeft -= 50;
});
}
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
@@ -267,28 +602,74 @@ export default {
removeConnected: 'workspaces/removeConnected',
selectTab: 'workspaces/selectTab',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab'
removeTab: 'workspaces/removeTab',
updateTabs: 'workspaces/updateTabs'
}),
addTab () {
this.newTab({ uid: this.connection.uid });
addQueryTab () {
this.newTab({ uid: this.connection.uid, type: 'query' });
},
getSelectedTab () {
return this.workspace.tabs.find(tab => tab.uid === this.selectedTab);
},
onKey (e) {
e.stopPropagation();
if (!this.hasWheelEvent) {
this.$refs.tabWrap.addEventListener('wheel', e => {
if (e.deltaY > 0) this.$refs.tabWrap.scrollLeft += 50;
else this.$refs.tabWrap.scrollLeft -= 50;
});
this.hasWheelEvent = true;
if (!this.isSelected)
return;
if ((e.ctrlKey || e.metaKey) && e.keyCode === 84 && !e.altKey) { // CTRL|Command + t
this.addQueryTab();
}
if ((e.ctrlKey || e.metaKey) && e.keyCode === 87 && !e.altKey) { // CTRL|Command + w
const currentTab = this.getSelectedTab();
if (currentTab)
this.closeTab(currentTab);
}
},
closeTab (tUid) {
if (this.queryTabs.length === 1) return;
this.removeTab({ uid: this.connection.uid, tab: tUid });
openAsPermanentTab (tab) {
const permanentTabs = {
table: 'data',
view: 'data',
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
scheduler: 'scheduler-props'
};
this.newTab({
uid: this.connection.uid,
schema: tab.schema,
elementName: tab.elementName,
type: permanentTabs[tab.elementType],
elementType: tab.elementType
});
},
closeTab (tab, force) {
this.unsavedTab = null;
// if (tab.type === 'query' && this.queryTabs.length === 1) return;
if (!force && tab.isChanged) {
this.unsavedTab = tab;
return;
}
this.removeTab({ uid: this.connection.uid, tab: tab.uid });
},
showProcessesModal () {
this.isProcessesModal = true;
},
hideProcessesModal () {
this.isProcessesModal = false;
},
addWheelEvent () {
if (!this.hasWheelEvent) {
this.$refs.tabWrap.$el.addEventListener('wheel', e => {
if (e.deltaY > 0) this.$refs.tabWrap.$el.scrollLeft += 50;
else this.$refs.tabWrap.$el.scrollLeft -= 50;
});
this.hasWheelEvent = true;
}
}
}
};
@@ -317,20 +698,35 @@ export default {
}
.tab-item {
max-width: 12rem;
width: fit-content;
flex: initial;
> a {
padding: 0.2rem 0.8rem;
padding: 0.2rem 0.6rem;
cursor: pointer;
display: flex;
align-items: center;
opacity: 0.7;
transition: opacity 0.2s;
&.badge::after {
position: absolute;
right: 35px;
top: 25px;
}
.btn-clear {
margin-left: 0.5rem;
opacity: 0;
transition: opacity 0.2s;
}
&:hover {
opacity: 1;
.btn-clear {
opacity: 1;
}
}
&.tab-add {
@@ -349,9 +745,15 @@ export default {
&.active a {
opacity: 1;
.btn-clear {
opacity: 1;
}
}
&.tools-dropdown {
height: 34px;
.tab-link:focus {
opacity: 1;
outline: 0;
@@ -362,7 +764,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;
@@ -406,7 +808,7 @@ export default {
.th {
position: sticky;
top: 0;
border: 1px solid;
border: 2px solid;
border-left: none;
border-bottom-width: 2px;
padding: 0;
@@ -415,15 +817,15 @@ export default {
z-index: 1;
> div {
padding: 0.1rem 0.4rem;
padding: 0.1rem 0.2rem;
min-width: -webkit-fill-available;
}
}
.td {
border-right: 1px solid;
border-bottom: 1px solid;
padding: 0 0.4rem;
border-right: 2px solid;
border-bottom: 2px solid;
padding: 0 0.2rem;
text-overflow: ellipsis;
max-width: 200px;
white-space: nowrap;

View File

@@ -0,0 +1,538 @@
<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>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.passphrase') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.sshPassphrase"
class="form-input"
type="password"
>
</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;
}
},
watch: {
'connection.client' () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
}
},
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 || res.response.toString() });
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 || res.response.toString() });
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

@@ -1,82 +0,0 @@
<template>
<div class="columns">
<div class="column col-12 empty">
<div class="empty-icon">
<i class="mdi mdi-48px mdi-power-plug-off" />
</div>
<p class="empty-title h5">
{{ isConnecting ? $t('word.connecting') : $t('word.disconnected') }}
</p>
<div class="empty-action">
<button
class="btn btn-success"
:class="{'loading': isConnecting}"
@click="startConnection"
>
{{ $t('word.connect') }}
</button>
</div>
</div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import ModalAskCredentials from '@/components/ModalAskCredentials';
export default {
name: 'WorkspaceConnectPanel',
components: {
ModalAskCredentials
},
props: {
connection: Object
},
data () {
return {
isConnecting: false,
isAsking: false
};
},
methods: {
...mapActions({
connectWorkspace: 'workspaces/connectWorkspace'
}),
async startConnection () {
this.isConnecting = true;
if (this.connection.ask)
this.isAsking = true;
else {
await this.connectWorkspace(this.connection);
this.isConnecting = false;
}
},
async continueTest (credentials) { // if "Ask for credentials" is true
this.isAsking = false;
const params = Object.assign({}, this.connection, credentials);
await this.connectWorkspace(params);
this.isConnecting = false;
},
closeAsking () {
this.isAsking = false;
this.isConnecting = false;
}
}
};
</script>
<style scoped>
.empty {
height: 100%;
border-radius: 0;
background: transparent;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,513 @@
<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>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.passphrase') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.sshPassphrase"
class="form-input"
type="password"
>
</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 || res.response.toString() });
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 || res.response.toString() });
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;
this.isConnecting = 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

@@ -0,0 +1,64 @@
<template>
<div class="column col-12 empty">
<div class="empty-icon">
<img
v-if="applicationTheme === 'dark'"
src="../images/logo-dark.svg"
width="200"
>
<img
v-if="applicationTheme === 'light'"
src="../images/logo-light.svg"
width="200"
>
</div>
<p class="h6 empty-subtitle">
{{ $t('message.noOpenTabs') }}
</p>
<div class="empty-action">
<button class="btn btn-gray d-flex" @click="$emit('new-tab')">
<i class="mdi mdi-24px mdi-tab-plus mr-2" />
{{ $t('message.openNewTab') }}
</button>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
name: 'WorkspaceEmptyState',
computed: {
...mapGetters({
applicationTheme: 'settings/getApplicationTheme',
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
}
},
created () {
this.changeBreadcrumbs({ schema: this.workspace.breadcrumbs.schema });
},
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
})
}
};
</script>
<style scoped>
.empty {
height: 100%;
border-radius: 0;
background: transparent;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-left: auto;
margin-right: auto;
}
</style>

View File

@@ -5,10 +5,13 @@
ref="explorebar"
class="workspace-explorebar column"
:style="{width: localWidth ? localWidth+'px' : ''}"
tabindex="0"
@keypress="explorebarSearch"
@keydown="explorebarSearch"
>
<div class="workspace-explorebar-header">
<span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connection_status === 'connected'" class="workspace-explorebar-tools">
<span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools">
<i
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewSchema')"
@@ -21,29 +24,30 @@
@click="refresh"
/>
<i
class="mdi mdi-18px mdi-power-plug-off c-hand"
class="mdi mdi-18px mdi-power c-hand"
:title="$t('word.disconnect')"
@click="disconnectWorkspace(connection.uid)"
/>
</span>
</div>
<div class="workspace-explorebar-search">
<div v-if="workspace.connection_status === 'connected'" class="has-icon-right">
<div v-if="workspace.connectionStatus === 'connected'" class="has-icon-right">
<input
ref="searchInput"
v-model="searchTerm"
class="form-input input-sm"
type="text"
:placeholder="$t('message.searchForElements')"
>
<i class="form-icon mdi mdi-magnify mdi-18px" />
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px" />
<i
v-else
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-1"
@click="searchTerm = ''"
/>
</div>
</div>
<WorkspaceConnectPanel
v-if="workspace.connection_status !== 'connected'"
class="workspace-explorebar-body"
:connection="connection"
/>
<div v-else class="workspace-explorebar-body">
<div class="workspace-explorebar-body" @click="$refs.explorebar.focus()">
<WorkspaceExploreBarSchema
v-for="db of workspace.structure"
:key="db.name"
@@ -52,6 +56,7 @@
@show-schema-context="openSchemaContext"
@show-table-context="openTableContext"
@show-misc-context="openMiscContext"
@show-misc-folder-context="openMiscFolderContext"
/>
</div>
</div>
@@ -60,69 +65,51 @@
@close="hideNewDBModal"
@reload="refresh"
/>
<ModalNewTable
v-if="isNewTableModal"
:workspace="workspace"
@close="hideCreateTableModal"
@open-create-table-editor="openCreateTableEditor"
/>
<ModalNewView
v-if="isNewViewModal"
:workspace="workspace"
@close="hideCreateViewModal"
@open-create-view-editor="openCreateViewEditor"
/>
<ModalNewTrigger
v-if="isNewTriggerModal"
:workspace="workspace"
@close="hideCreateTriggerModal"
@open-create-trigger-editor="openCreateTriggerEditor"
/>
<ModalNewRoutine
v-if="isNewRoutineModal"
:workspace="workspace"
@close="hideCreateRoutineModal"
@open-create-routine-editor="openCreateRoutineEditor"
/>
<ModalNewFunction
v-if="isNewFunctionModal"
:workspace="workspace"
@close="hideCreateFunctionModal"
@open-create-function-editor="openCreateFunctionEditor"
/>
<ModalNewScheduler
v-if="isNewSchedulerModal"
:workspace="workspace"
@close="hideCreateSchedulerModal"
@open-create-scheduler-editor="openCreateSchedulerEditor"
/>
<DatabaseContext
v-if="isDatabaseContext"
:selected-database="selectedDatabase"
:selected-schema="selectedSchema"
:context-event="databaseContextEvent"
@close-context="closeDatabaseContext"
@show-create-table-modal="showCreateTableModal"
@show-create-view-modal="showCreateViewModal"
@show-create-trigger-modal="showCreateTriggerModal"
@show-create-routine-modal="showCreateRoutineModal"
@show-create-function-modal="showCreateFunctionModal"
@show-create-scheduler-modal="showCreateSchedulerModal"
@open-create-table-tab="openCreateElementTab('table')"
@open-create-view-tab="openCreateElementTab('view')"
@open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-routine-tab="openCreateElementTab('routine')"
@open-create-function-tab="openCreateElementTab('function')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-scheduler-tab="openCreateElementTab('scheduler')"
@reload="refresh"
/>
<TableContext
v-if="isTableContext"
:selected-schema="selectedSchema"
:selected-table="selectedTable"
:context-event="tableContextEvent"
@delete-table="deleteTable"
@duplicate-table="duplicateTable"
@close-context="closeTableContext"
@reload="refresh"
/>
<MiscContext
v-if="isMiscContext"
:selected-misc="selectedMisc"
:selected-schema="selectedSchema"
:context-event="miscContextEvent"
@close-context="closeMiscContext"
@reload="refresh"
/>
<MiscFolderContext
v-if="isMiscFolderContext"
:selected-misc="selectedMisc"
:selected-schema="selectedSchema"
:context-event="miscContextEvent"
@open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-routine-tab="openCreateElementTab('routine')"
@open-create-function-tab="openCreateElementTab('function')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-scheduler-tab="openCreateElementTab('scheduler')"
@close-context="closeMiscFolderContext"
@reload="refresh"
/>
</div>
</template>
@@ -131,39 +118,25 @@ import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
import Triggers from '@/ipc-api/Triggers';
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 ModalNewScheduler from '@/components/ModalNewScheduler';
export default {
name: 'WorkspaceExploreBar',
components: {
WorkspaceConnectPanel,
WorkspaceExploreBarSchema,
DatabaseContext,
TableContext,
MiscContext,
ModalNewSchema,
ModalNewTable,
ModalNewView,
ModalNewTrigger,
ModalNewRoutine,
ModalNewFunction,
ModalNewScheduler
MiscFolderContext,
ModalNewSchema
},
props: {
connection: Object,
@@ -174,11 +147,11 @@ export default {
isRefreshing: false,
isNewDBModal: false,
isNewTableModal: false,
isNewViewModal: false,
isNewTriggerModal: false,
isNewRoutineModal: false,
isNewFunctionModal: false,
isNewTriggerFunctionModal: false,
isNewSchedulerModal: false,
localWidth: null,
@@ -187,12 +160,13 @@ export default {
isDatabaseContext: false,
isTableContext: false,
isMiscContext: false,
isMiscFolderContext: false,
databaseContextEvent: null,
tableContextEvent: null,
miscContextEvent: null,
selectedDatabase: '',
selectedSchema: '',
selectedTable: null,
selectedMisc: null,
searchTerm: ''
@@ -209,6 +183,9 @@ export default {
},
connectionName () {
return this.getConnectionName(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
}
},
watch: {
@@ -249,9 +226,13 @@ export default {
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
selectTab: 'workspaces/selectTab',
newTab: 'workspaces/newTab',
removeTabs: 'workspaces/removeTabs',
setSearchTerm: 'workspaces/setSearchTerm',
addNotification: 'notifications/addNotification',
changeExplorebarSize: 'settings/changeExplorebarSize'
changeExplorebarSize: 'settings/changeExplorebarSize',
addLoadingElement: 'workspaces/addLoadingElement',
removeLoadingElement: 'workspaces/removeLoadingElement'
}),
async refresh () {
if (!this.isRefreshing) {
@@ -260,6 +241,19 @@ export default {
this.isRefreshing = false;
}
},
explorebarSearch (e) {
if (e.code === 'Backspace') {
e.preventDefault();
if (this.searchTerm.length)
this.searchTerm = this.searchTerm.slice(0, -1);
else
return;
}
else if (e.key.length > 1)// Prevent non-alphanumerics
return;
this.$refs.searchInput.focus();
},
resize (e) {
const el = this.$refs.explorebar;
let explorebarWidth = e.pageX - el.getBoundingClientRect().left;
@@ -276,31 +270,20 @@ export default {
hideNewDBModal () {
this.isNewDBModal = false;
},
showCreateTableModal () {
openCreateElementTab (element) {
this.closeDatabaseContext();
this.isNewTableModal = true;
},
hideCreateTableModal () {
this.isNewTableModal = false;
},
async openCreateTableEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
this.closeMiscFolderContext();
const { status, response } = await Tables.createTable(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, table: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: '',
elementType: element,
type: `new-${element}`
});
},
openSchemaContext (payload) {
this.selectedDatabase = payload.schema;
this.selectedSchema = payload.schema;
this.databaseContextEvent = payload.event;
this.isDatabaseContext = true;
},
@@ -309,6 +292,7 @@ export default {
},
openTableContext (payload) {
this.selectedTable = payload.table;
this.selectedSchema = payload.schema;
this.tableContextEvent = payload.event;
this.isTableContext = true;
},
@@ -317,98 +301,149 @@ export default {
},
openMiscContext (payload) {
this.selectedMisc = payload.misc;
this.selectedSchema = payload.schema;
this.miscContextEvent = payload.event;
this.isMiscContext = true;
},
openMiscFolderContext (payload) {
this.selectedMisc = payload.type;
this.selectedSchema = payload.schema;
this.miscContextEvent = payload.event;
this.isMiscFolderContext = true;
},
closeMiscContext () {
this.isMiscContext = false;
},
showCreateViewModal () {
this.closeDatabaseContext();
this.isNewViewModal = true;
},
hideCreateViewModal () {
this.isNewViewModal = false;
},
async openCreateViewEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Views.createView(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, view: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
closeMiscFolderContext () {
this.isMiscFolderContext = false;
},
showCreateTriggerModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewTriggerModal = true;
},
hideCreateTriggerModal () {
this.isNewTriggerModal = false;
},
async openCreateTriggerEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Triggers.createTrigger(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, trigger: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateRoutineModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewRoutineModal = true;
},
hideCreateRoutineModal () {
this.isNewRoutineModal = false;
},
async openCreateRoutineEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Routines.createRoutine(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, procedure: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
},
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 () {
this.isNewSchedulerModal = false;
},
async deleteTable (payload) {
this.closeTableContext();
this.addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
let res;
if (payload.table.type === 'table') {
res = await Tables.dropTable({
uid: this.connection.uid,
table: payload.table.name,
schema: payload.schema
});
}
else if (payload.table.type === 'view') {
res = await Views.dropView({
uid: this.connection.uid,
view: payload.table.name,
schema: payload.schema
});
}
const { status, response } = res;
if (status === 'success') {
this.refresh();
this.removeTabs({
uid: this.connection.uid,
elementName: payload.table.name,
elementType: payload.table.type,
schema: payload.schema
});
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
},
async duplicateTable (payload) {
this.closeTableContext();
this.addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
const { status, response } = await Tables.duplicateTable({
uid: this.connection.uid,
table: payload.table.name,
schema: payload.schema
});
if (status === 'success')
this.refresh();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
},
async openCreateFunctionEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
@@ -416,8 +451,39 @@ export default {
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, function: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
this.changeBreadcrumbs({ schema: this.selectedSchema, function: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'function',
type: 'function-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
async openCreateTriggerFunctionEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Functions.createTriggerFunction(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, triggerFunction: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'triggerFunction',
type: 'trigger-function-props'
});
}
else
this.addNotification({ status: 'error', message: response });
@@ -425,6 +491,7 @@ export default {
async openCreateSchedulerEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
@@ -432,8 +499,15 @@ export default {
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, scheduler: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
this.changeBreadcrumbs({ schema: this.selectedSchema, scheduler: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'scheduler',
type: 'scheduler-props'
});
}
else
this.addNotification({ status: 'error', message: response });
@@ -451,6 +525,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 {
@@ -465,6 +544,10 @@ export default {
position: relative;
padding: 0;
&:focus {
outline: none;
}
.workspace-explorebar-header {
width: 100%;
padding: 0.3rem;

View File

@@ -10,6 +10,30 @@
>
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
</div>
<div
v-if="selectedMisc.type === 'trigger' && customizations.triggerEnableDisable"
class="context-element"
@click="toggleTrigger"
>
<span v-if="!selectedMisc.enabled" class="d-flex">
<i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.enable') }}
</span>
<span v-else class="d-flex">
<i class="mdi mdi-18px mdi-pause text-light pr-1" /> {{ $t('word.disable') }}
</span>
</div>
<div
v-if="selectedMisc.type === 'scheduler'"
class="context-element"
@click="toggleScheduler"
>
<span v-if="!selectedMisc.enabled" class="d-flex">
<i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.enable') }}
</span>
<span v-else class="d-flex">
<i class="mdi mdi-18px mdi-pause text-light pr-1" /> {{ $t('word.disable') }}
</span>
</div>
<div class="context-element" @click="showDeleteModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div>
@@ -20,7 +44,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">
@@ -58,7 +83,8 @@ export default {
},
props: {
contextEvent: MouseEvent,
selectedMisc: Object
selectedMisc: Object,
selectedSchema: String
},
data () {
return {
@@ -76,6 +102,9 @@ export default {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.getWorkspace(this.selectedWorkspace).customizations;
},
deleteMessage () {
switch (this.selectedMisc.type) {
case 'trigger':
@@ -83,6 +112,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');
@@ -95,11 +125,11 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
addLoadingElement: 'workspaces/addLoadingElement',
removeLoadingElement: 'workspaces/removeLoadingElement',
removeTabs: 'workspaces/removeTabs',
newTab: 'workspaces/newTab'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},
@@ -124,24 +154,29 @@ export default {
case 'trigger':
res = await Triggers.dropTrigger({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
trigger: this.selectedMisc.name
});
break;
case 'procedure':
res = await Routines.dropRoutine({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
routine: this.selectedMisc.name
});
break;
case 'function':
case 'triggerFunction':
res = await Functions.dropFunction({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
func: this.selectedMisc.name
});
break;
case 'scheduler':
res = await Schedulers.dropScheduler({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
scheduler: this.selectedMisc.name
});
break;
@@ -150,7 +185,12 @@ export default {
const { status, response } = res;
if (status === 'success') {
this.changeBreadcrumbs({ [this.selectedMisc.type]: null });
this.removeTabs({
uid: this.selectedWorkspace,
elementName: this.selectedMisc.name,
elementType: this.selectedMisc.type,
schema: this.selectedSchema
});
this.closeContext();
this.$emit('reload');
@@ -177,8 +217,8 @@ export default {
async runRoutineCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
routine: this.workspace.breadcrumbs.procedure
schema: this.selectedSchema,
routine: this.selectedMisc.name
};
try {
@@ -215,14 +255,14 @@ export default {
sql = `CALL \`${this.localElement.name}\`(${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.newTab({ uid: this.workspace.uid, content: sql, type: 'query', autorun: true });
this.closeContext();
},
async runFunctionCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
func: this.workspace.breadcrumbs.function
schema: this.selectedSchema,
func: this.selectedMisc.name
};
try {
@@ -260,8 +300,70 @@ export default {
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.newTab({ uid: this.workspace.uid, content: sql, type: 'query', autorun: true });
this.closeContext();
},
async toggleTrigger () {
this.addLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'trigger'
});
try {
const { status, response } = await Triggers.toggleTrigger({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
trigger: this.selectedMisc.name,
enabled: this.selectedMisc.enabled
});
if (status !== 'success')
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'trigger'
});
this.closeContext();
this.$emit('reload');
},
async toggleScheduler () {
this.addLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'scheduler'
});
try {
const { status, response } = await Schedulers.toggleScheduler({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
scheduler: this.selectedMisc.name,
enabled: this.selectedMisc.enabled
});
if (status !== 'success')
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'scheduler'
});
this.closeContext();
this.$emit('reload');
}
}
};

View File

@@ -0,0 +1,95 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<div
v-if="selectedMisc === 'trigger'"
class="context-element"
@click="$emit('open-create-trigger-tab')"
>
<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('open-create-routine-tab')"
>
<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('open-create-function-tab')"
>
<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('open-create-trigger-function-tab')"
>
<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('open-create-scheduler-tab')"
>
<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,
selectedSchema: 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'
}),
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

@@ -9,21 +9,37 @@
<div v-if="isLoading" class="icon loading" />
<i v-else class="icon mdi mdi-18px mdi-chevron-right" />
<i class="database-icon mdi mdi-18px mdi-database mr-1" />
<span>{{ database.name }}</span>
<div class="">
<span>{{ database.name }}</span>
<div
v-if="database.size"
class="schema-size tooltip tooltip-left mr-1"
:data-tooltip="formatBytes(database.size)"
>
<i class="mdi mdi-information-outline pr-2" />
</div>
</div>
</summary>
<div class="accordion-body">
<div class="database-tables">
<ul class="menu menu-nav pt-0">
<li
v-for="table of filteredTables"
:ref="breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name) ? 'explorebar-selected' : ''"
:key="table.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@click="setBreadcrumbs({schema: database.name, [table.type]: table.name})"
:class="{'selected': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@mousedown.left="selectTable({schema: database.name, table})"
@dblclick="openDataTab({schema: database.name, table})"
@contextmenu.prevent="showTableContext($event, table)"
>
<a class="table-name">
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<div v-if="checkLoadingStatus(table.name, 'table')" class="icon loading mr-1" />
<i
v-else
class="table-icon mdi mdi-18px mr-1"
:class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'"
/>
<span v-html="highlightWord(table.name)" />
</a>
<div
@@ -39,7 +55,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>
@@ -49,15 +69,25 @@
<li
v-for="trigger of filteredTriggers"
:key="trigger.name"
:ref="breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@click="setBreadcrumbs({schema: database.name, trigger: trigger.name})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@mousedown.left="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: trigger, type: 'trigger'})"
@contextmenu.prevent="showMiscContext($event, {...trigger, type: 'trigger'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<div v-if="checkLoadingStatus(trigger.name, 'trigger')" class="icon loading mr-1" />
<i v-else class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<span v-html="highlightWord(trigger.name)" />
</a>
<div
v-if="trigger.enabled === false"
class="tooltip tooltip-left disabled-indicator"
:data-tooltip="$t('word.disabled')"
>
<i class="table-icon mdi mdi-pause mdi-18px mr-1" />
</div>
</li>
</ul>
</div>
@@ -67,7 +97,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.routine}"
@contextmenu.prevent="showMiscFolderContext($event, 'procedure')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
{{ $tc('word.storedRoutine', 2) }}
</summary>
@@ -77,9 +111,11 @@
<li
v-for="(procedure, i) of filteredProcedures"
:key="`${procedure.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@click="setBreadcrumbs({schema: database.name, procedure: procedure.name})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name}"
@mousedown.left="selectMisc({schema: database.name, misc: procedure, type: 'routine'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: procedure, type: 'routine'})"
@contextmenu.prevent="showMiscContext($event, {...procedure, type: 'procedure'})"
>
<a class="table-name">
@@ -93,9 +129,47 @@
</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}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: func, type: 'triggerFunction'})"
@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>
@@ -105,9 +179,11 @@
<li
v-for="(func, i) of filteredFunctions"
:key="`${func.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.function === func.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@click="setBreadcrumbs({schema: database.name, function: func.name})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'function'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: func, type: 'function'})"
@contextmenu.prevent="showMiscContext($event, {...func, type: 'function'})"
>
<a class="table-name">
@@ -123,7 +199,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>
@@ -133,15 +213,25 @@
<li
v-for="scheduler of filteredSchedulers"
:key="scheduler.name"
:ref="breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@click="setBreadcrumbs({schema: database.name, scheduler: scheduler.name})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@mousedown.left="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: scheduler, type: 'scheduler'})"
@contextmenu.prevent="showMiscContext($event, {...scheduler, type: 'scheduler'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<div v-if="checkLoadingStatus(scheduler.name, 'scheduler')" class="icon loading mr-1" />
<i v-else class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<span v-html="highlightWord(scheduler.name)" />
</a>
<div
v-if="scheduler.enabled === false"
class="tooltip tooltip-left disabled-indicator"
:data-tooltip="$t('word.disabled')"
>
<i class="table-icon mdi mdi-pause mdi-18px mr-1" />
</div>
</li>
</ul>
</div>
@@ -189,14 +279,22 @@ 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);
},
workspace () {
return this.getWorkspace(this.connection.uid);
},
breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs;
return this.workspace.breadcrumbs;
},
customizations () {
return this.getWorkspace(this.connection.uid).customizations;
return this.workspace.customizations;
},
loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid);
@@ -211,32 +309,106 @@ export default {
return this.database.tables.reduce((acc, curr) => acc + curr.size, 0);
}
},
watch: {
breadcrumbs (newVal, oldVal) {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
setTimeout(() => {
const element = this.$refs['explorebar-selected'] ? this.$refs['explorebar-selected'][0] : null;
if (element) {
const rect = element.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
const isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
if (!isVisible) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
}
}, 50);
}
}
},
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
addLoadedSchema: 'workspaces/addLoadedSchema',
newTab: 'workspaces/newTab',
refreshSchema: 'workspaces/refreshSchema'
}),
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.addLoadedSchema(schema);
this.isLoading = false;
}
},
selectTable ({ schema, table }) {
this.newTab({
uid: this.connection.uid,
elementName: table.name,
schema: this.database.name,
type: 'temp-data',
elementType: table.type
});
this.changeBreadcrumbs({ schema, table: null });
this.setBreadcrumbs({ schema, [table.type]: table.name });
},
selectMisc ({ schema, misc, type }) {
const miscTempTabs = {
trigger: 'temp-trigger-props',
triggerFunction: 'temp-trigger-function-props',
function: 'temp-function-props',
routine: 'temp-routine-props',
scheduler: 'temp-scheduler-props'
};
this.newTab({
uid: this.connection.uid,
elementName: misc.name,
schema: this.database.name,
type: miscTempTabs[type],
elementType: type
});
this.setBreadcrumbs({ schema, [type]: misc.name });
},
openDataTab ({ schema, table }) {
this.newTab({ uid: this.connection.uid, elementName: table.name, schema: this.database.name, type: 'data', elementType: table.type });
this.setBreadcrumbs({ schema, [table.type]: table.name });
},
openMiscPermanentTab ({ schema, misc, type }) {
const miscTabs = {
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
scheduler: 'scheduler-props'
};
this.newTab({
uid: this.connection.uid,
elementName: misc.name,
schema: this.database.name,
type: miscTabs[type],
elementType: type
});
this.setBreadcrumbs({ schema, [type]: misc.name });
},
showSchemaContext (event, schema) {
this.selectSchema(schema);
this.$emit('show-schema-context', { event, schema });
},
showTableContext (event, table) {
this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name });
this.$emit('show-table-context', { event, table });
this.$emit('show-table-context', { event, schema: this.database.name, table });
},
showMiscContext (event, misc) {
this.setBreadcrumbs({ schema: this.database.name, [misc.type]: misc.name });
this.$emit('show-misc-context', { event, misc });
this.$emit('show-misc-context', { event, schema: this.database.name, misc });
},
showMiscFolderContext (event, type) {
this.$emit('show-misc-folder-context', { event, schema: this.database.name, type });
},
piePercentage (val) {
const perc = val / this.maxSize * 100;
@@ -250,12 +422,20 @@ export default {
this.changeBreadcrumbs(payload);
},
highlightWord (string) {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary">$1</span>');
}
else
return string;
},
checkLoadingStatus (name, type) {
return this.workspace.loadingElements.some(el =>
el.name === name &&
el.type === type &&
el.schema === this.database.name);
}
}
};
@@ -263,10 +443,19 @@ export default {
<style lang="scss">
.workspace-explorebar-database {
.selected {
font-weight: 700;
}
.database-name {
position: sticky;
top: 0;
z-index: 2;
.schema-size{
visibility: hidden;
width: 22.5px;
}
}
.database-name,
@@ -311,7 +500,11 @@ export default {
.database-name,
.misc-name {
&:hover {
border-radius: 2px;
border-radius: $border-radius;
.schema-size{
visibility: visible;
}
}
}
@@ -319,8 +512,9 @@ export default {
line-height: 1.2;
position: relative;
&:hover {
border-radius: 2px;
&:hover,
&.selected {
border-radius: $border-radius;
}
}
@@ -340,7 +534,9 @@ export default {
}
}
.table-size {
.schema-size,
.table-size,
.disabled-indicator {
position: absolute;
right: 0;
top: 0;

View File

@@ -10,42 +10,49 @@
<div
v-if="workspace.customizations.tableAdd"
class="context-element"
@click="showCreateTableModal"
@click="openCreateTableTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div>
<div
v-if="workspace.customizations.viewAdd"
class="context-element"
@click="showCreateViewModal"
@click="openCreateViewTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
</div>
<div
v-if="workspace.customizations.triggerAdd"
class="context-element"
@click="showCreateTriggerModal"
@click="openCreateTriggerTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div>
<div
v-if="workspace.customizations.routineAdd"
class="context-element"
@click="showCreateRoutineModal"
@click="openCreateRoutineTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div>
<div
v-if="workspace.customizations.functionAdd"
class="context-element"
@click="showCreateFunctionModal"
@click="openCreateFunctionTab"
>
<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="openCreateTriggerFunctionTab"
>
<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"
@click="showCreateSchedulerModal"
@click="openCreateSchedulerTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div>
@@ -69,18 +76,19 @@
>
<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">
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedDatabase }}</b>"?
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedSchema }}</b>"?
</div>
</div>
</ConfirmModal>
<ModalEditSchema
v-if="isEditModal"
:selected-database="selectedDatabase"
:selected-schema="selectedSchema"
@close="hideEditModal"
/>
</BaseContextMenu>
@@ -102,7 +110,7 @@ export default {
},
props: {
contextEvent: MouseEvent,
selectedDatabase: String
selectedSchema: String
},
data () {
return {
@@ -124,23 +132,26 @@ export default {
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
openCreateTableTab () {
this.$emit('open-create-table-tab');
},
showCreateViewModal () {
this.$emit('show-create-view-modal');
openCreateViewTab () {
this.$emit('open-create-view-tab');
},
showCreateTriggerModal () {
this.$emit('show-create-trigger-modal');
openCreateTriggerTab () {
this.$emit('open-create-trigger-tab');
},
showCreateRoutineModal () {
this.$emit('show-create-routine-modal');
openCreateRoutineTab () {
this.$emit('open-create-routine-tab');
},
showCreateFunctionModal () {
this.$emit('show-create-function-modal');
openCreateFunctionTab () {
this.$emit('open-create-function-tab');
},
showCreateSchedulerModal () {
this.$emit('show-create-scheduler-modal');
openCreateTriggerFunctionTab () {
this.$emit('open-create-trigger-function-tab');
},
openCreateSchedulerTab () {
this.$emit('open-create-scheduler-tab');
},
showDeleteModal () {
this.isDeleteModal = true;
@@ -162,11 +173,11 @@ export default {
try {
const { status, response } = await Schema.deleteSchema({
uid: this.selectedWorkspace,
database: this.selectedDatabase
database: this.selectedSchema
});
if (status === 'success') {
if (this.selectedDatabase === this.workspace.breadcrumbs.schema)
if (this.selectedSchema === this.workspace.breadcrumbs.schema)
this.changeBreadcrumbs({ schema: null });
this.closeContext();

View File

@@ -4,7 +4,28 @@
@close-context="closeContext"
>
<div
v-if="selectedTable.type === 'table'"
v-if="selectedTable && selectedTable.type === 'table' && customizations.tableSettings"
class="context-element"
@click="openTableSettingTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-tune-vertical-variant text-light pr-1" /> {{ $t('word.settings') }}</span>
</div>
<div
v-if="selectedTable && selectedTable.type === 'view' && customizations.viewSettings"
class="context-element"
@click="openViewSettingTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-tune-vertical-variant text-light pr-1" /> {{ $t('word.settings') }}</span>
</div>
<div
v-if="selectedTable && 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 && selectedTable.type === 'table'"
class="context-element"
@click="showEmptyModal"
>
@@ -21,7 +42,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 +58,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">
@@ -54,7 +76,6 @@ import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
export default {
name: 'WorkspaceExploreBarTableContext',
@@ -64,7 +85,8 @@ export default {
},
props: {
contextEvent: MouseEvent,
selectedTable: Object
selectedTable: Object,
selectedSchema: String
},
data () {
return {
@@ -79,16 +101,20 @@ export default {
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.workspace && this.workspace.customizations ? this.workspace.customizations : {};
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
newTab: 'workspaces/newTab',
removeTabs: 'workspaces/removeTabs',
addLoadingElement: 'workspaces/addLoadingElement',
removeLoadingElement: 'workspaces/removeLoadingElement',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},
@@ -104,59 +130,74 @@ export default {
closeContext () {
this.$emit('close-context');
},
openTableSettingTab () {
this.newTab({
uid: this.selectedWorkspace,
elementName: this.selectedTable.name,
schema: this.selectedSchema,
type: 'table-props',
elementType: 'table'
});
this.changeBreadcrumbs({
schema: this.selectedSchema,
table: this.selectedTable.name
});
this.closeContext();
},
openViewSettingTab () {
this.newTab({
uid: this.selectedWorkspace,
elementType: 'table',
elementName: this.selectedTable.name,
schema: this.selectedSchema,
type: 'view-props'
});
this.changeBreadcrumbs({
schema: this.selectedSchema,
view: this.selectedTable.name
});
this.closeContext();
},
duplicateTable () {
this.$emit('duplicate-table', { schema: this.selectedSchema, table: this.selectedTable });
},
async emptyTable () {
this.closeContext();
this.addLoadingElement({
name: this.selectedTable.name,
schema: this.selectedSchema,
type: 'table'
});
try {
const { status, response } = await Tables.truncateTable({
uid: this.selectedWorkspace,
table: this.selectedTable.name
table: this.selectedTable.name,
schema: this.selectedSchema
});
if (status === 'success') {
if (this.selectedTable.name === this.workspace.breadcrumbs.table)
this.changeBreadcrumbs({ table: null });
this.closeContext();
if (status === 'success')
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: this.selectedTable.name,
schema: this.selectedSchema,
type: 'table'
});
},
async deleteTable () {
try {
let res;
if (this.selectedTable.type === 'table') {
res = await Tables.dropTable({
uid: this.selectedWorkspace,
table: this.selectedTable.name
});
}
else if (this.selectedTable.type === 'view') {
res = await Views.dropView({
uid: this.selectedWorkspace,
view: this.selectedTable.name
});
}
const { status, response } = res;
if (status === 'success') {
if (this.selectedTable.name === this.workspace.breadcrumbs.table || this.selectedTable.name === this.workspace.breadcrumbs.view)
this.changeBreadcrumbs({ table: null, view: null });
this.closeContext();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
deleteTable () {
this.$emit('delete-table', { schema: this.selectedSchema, table: this.selectedTable });
}
}
};

View File

@@ -1,212 +0,0 @@
<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" /> {{ $t('word.options') }} "{{ localOptions.name }}"
</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="optionsProxy.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
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="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>
<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 class="form-group">
<label class="form-label col-4">
{{ $t('word.returns') }}
</label>
<div class="column">
<div class="input-group">
<select
v-model="optionsProxy.returns"
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"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="optionsProxy.returns === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
<input
v-if="customizations.parametersLength"
v-model="optionsProxy.returnsLength"
class="form-input"
type="number"
min="0"
>
</div>
</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>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="optionsProxy.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="optionsProxy.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="optionsProxy.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsFunctionOptionsModal',
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,130 +0,0 @@
<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" /> {{ $t('word.options') }} "{{ table }}"
</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="optionsProxy.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
type="text"
>
</div>
</div>
<div v-if="workspace.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>
<div v-if="workspace.customizations.autoIncrement" class="form-group">
<label class="form-label col-4">
{{ $t('word.autoIncrement') }}
</label>
<div class="column">
<input
v-model="optionsProxy.autoIncrement"
class="form-input"
type="number"
>
</div>
</div>
<div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4">
{{ $t('word.collation') }}
</label>
<div class="column">
<select v-model="optionsProxy.collation" class="form-select">
<option
v-for="collation in workspace.collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</div>
</div>
<div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4">
{{ $t('word.engine') }}
</label>
<div class="column">
<select v-model="optionsProxy.engine" class="form-select">
<option
v-for="engine in workspace.engines"
:key="engine.name"
:value="engine.name"
>
{{ engine.name }}
</option>
</select>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
table: String,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
}
},
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,160 +0,0 @@
<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" /> {{ $t('word.options') }} "{{ localOptions.name }}"
</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="optionsProxy.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
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="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>
<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>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="optionsProxy.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="optionsProxy.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="optionsProxy.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
}
},
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,50 +0,0 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<div
v-if="selectedRows.length === 1"
class="context-element"
@click="setNull"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-null text-light pr-1" /> {{ $t('message.setNull') }}
</span>
</div>
<div class="context-element" @click="showConfirmModal">
<span class="d-flex">
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</span>
</div>
</BaseContextMenu>
</template>
<script>
import BaseContextMenu from '@/components/BaseContextMenu';
export default {
name: 'WorkspaceQueryTableContext',
components: {
BaseContextMenu
},
props: {
contextEvent: MouseEvent,
selectedRows: Array
},
computed: {
},
methods: {
showConfirmModal () {
this.$emit('show-delete-modal');
},
closeContext () {
this.$emit('close-context');
},
setNull () {
this.$emit('set-null');
this.closeContext();
}
}
};
</script>

View File

@@ -0,0 +1,397 @@
<template>
<div v-show="isSelected" 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="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
</label>
<input
ref="firstInput"
v-model="localFunction.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
</label>
<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="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
</label>
<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 class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.returns') }}
</label>
<div class="input-group">
<select
v-model="localFunction.returns"
class="form-select text-uppercase"
style="max-width: 150px;"
>
<option v-if="localFunction.returns === 'VOID'">
VOID
</option>
<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-if="customizations.parametersLength"
v-model="localFunction.returnsLength"
style="max-width: 150px;"
class="form-input"
type="number"
min="0"
:placeholder="$t('word.length')"
>
</div>
</div>
</div>
<div v-if="customizations.comment" class="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
</label>
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
</label>
<select v-model="localFunction.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.functionDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
</label>
<select v-model="localFunction.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.functionDeterministic" class="column col-auto">
<div class="form-group">
<label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</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>
<WorkspaceTabPropsFunctionParamsModal
v-if="isParamsModal"
:local-parameters="localFunction.parameters"
:workspace="workspace"
:func="localFunction.name"
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspaceTabNewFunction',
components: {
BaseLoader,
QueryEditor,
WorkspaceTabPropsFunctionParamsModal
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
originalFunction: {},
localFunction: {},
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
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: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalFunction = {
sql: this.customizations.functionSql,
language: this.customizations.languages ? this.customizations.languages[0] : null,
name: '',
definer: '',
comment: '',
security: 'DEFINER',
dataAccess: 'CONTAINS SQL',
deterministic: false,
returns: this.workspace.dataTypes.length ? this.workspace.dataTypes[0].types[0].name : null
};
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localFunction
};
try {
const { status, response } = await Functions.createFunction(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localFunction.name,
elementType: 'function',
type: 'function-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, function: this.localFunction.name });
}
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 };
},
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

@@ -0,0 +1,353 @@
<template>
<div v-show="isSelected" 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="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
</label>
<input
ref="firstInput"
v-model="localRoutine.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
</label>
<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="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
</label>
<select
v-if="workspace.users.length"
v-model="localRoutine.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="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
</label>
<input
v-model="localRoutine.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
</label>
<select v-model="localRoutine.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
</label>
<select v-model="localRoutine.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDeterministic" class="column col-auto">
<div class="form-group">
<label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline">
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</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.routineBody') }}</label>
<QueryEditor
v-show="isSelected"
:key="`new-${_uid}`"
ref="queryEditor"
:value.sync="localRoutine.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspaceTabPropsRoutineParamsModal
v-if="isParamsModal"
:local-parameters="localRoutine.parameters"
:workspace="workspace"
routine="new"
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal';
import Routines from '@/ipc-api/Routines';
export default {
name: 'WorkspaceTabNewRoutine',
components: {
QueryEditor,
BaseLoader,
WorkspaceTabPropsRoutineParamsModal
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
originalRoutine: {},
localRoutine: {},
lastRoutine: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
isChanged () {
return JSON.stringify(this.originalRoutine) !== JSON.stringify(this.localRoutine);
},
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.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: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalRoutine = {
sql: this.customizations.procedureSql,
language: this.customizations.languages ? this.customizations.languages[0] : null,
name: '',
definer: '',
comment: '',
security: 'DEFINER',
dataAccess: 'CONTAINS SQL',
deterministic: false
};
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localRoutine
};
try {
const { status, response } = await Routines.createRoutine(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localRoutine.name,
elementType: 'routine',
type: 'routine-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, routine: this.localRoutine.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.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();
}
},
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
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

@@ -0,0 +1,323 @@
<template>
<div v-show="isSelected" 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="showTimingModal">
<i class="mdi mdi-24px mdi-timer mr-1" />
<span>{{ $t('word.timing') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localScheduler.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localScheduler.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalScheduler.definer">
{{ originalScheduler.definer.replaceAll('`', '') }}
</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 class="column">
<div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label>
<input
v-model="localScheduler.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="column">
<div class="form-group">
<label class="form-label mr-2">{{ $t('word.state') }}</label>
<label class="form-radio form-inline">
<input
v-model="localScheduler.state"
type="radio"
name="state"
value="ENABLE"
><i class="form-icon" /> ENABLE
</label>
<label class="form-radio form-inline">
<input
v-model="localScheduler.state"
type="radio"
name="state"
value="DISABLE"
><i class="form-icon" /> DISABLE
</label>
<label class="form-radio form-inline">
<input
v-model="localScheduler.state"
type="radio"
name="state"
value="DISABLE ON SLAVE"
><i class="form-icon" /> DISABLE ON SLAVE
</label>
</div>
</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.schedulerBody') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localScheduler.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspaceTabPropsSchedulerTimingModal
v-if="isTimingModal"
:local-options="localScheduler"
:workspace="workspace"
@hide="hideTimingModal"
@options-update="timingUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal';
import Schedulers from '@/ipc-api/Schedulers';
export default {
name: 'WorkspaceTabNewScheduler',
components: {
BaseLoader,
QueryEditor,
WorkspaceTabPropsSchedulerTimingModal
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isTimingModal: false,
originalScheduler: {},
localScheduler: {},
lastScheduler: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
tabUid () {
return this.$vnode.key;
},
isChanged () {
return JSON.stringify(this.originalScheduler) !== JSON.stringify(this.localScheduler);
},
isDefinerInUsers () {
return this.originalScheduler ? this.workspace.users.some(user => this.originalScheduler.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: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
this.originalScheduler = {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
comment: '',
execution: 'EVERY',
every: ['1', 'DAY'],
preserve: true,
state: 'DISABLE'
};
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localScheduler
};
try {
const { status, response } = await Schedulers.createScheduler(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localScheduler.name,
elementType: 'scheduler',
type: 'scheduler-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, scheduler: this.localScheduler.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.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();
}
},
showTimingModal () {
this.isTimingModal = true;
},
hideTimingModal () {
this.isTimingModal = false;
},
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();
}
}
}
}
};
</script>

View File

@@ -0,0 +1,452 @@
<template>
<div v-show="isSelected" 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 || isSaving"
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
:disabled="isSaving"
class="btn btn-dark btn-sm"
:title="$t('message.addNewField')"
@click="addField"
>
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $t('word.add') }}</span>
</button>
<button
:disabled="isSaving || !localFields.length"
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>
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isSaving || !localFields.length"
@click="showForeignModal"
>
<i class="mdi mdi-24px mdi-key-link mr-1" />
<span>{{ $t('word.foreignKeys') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localOptions.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.comment" class="column">
<div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label>
<input
v-model="localOptions.comment"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.collations" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.collation') }}
</label>
<select v-model="localOptions.collation" class="form-select">
<option
v-for="collation in workspace.collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</div>
</div>
<div v-if="workspace.customizations.engines" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.engine') }}
</label>
<select v-model="localOptions.engine" class="form-select">
<option
v-for="engine in workspace.engines"
:key="engine.name"
:value="engine.name"
>
{{ engine.name }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 p-relative">
<BaseLoader v-if="isLoading" />
<WorkspaceTabNewTableEmptyState v-if="!localFields.length" @new-field="addField" />
<WorkspaceTabPropsTableFields
v-if="localFields.length"
ref="indexTable"
:fields="localFields"
:indexes="localIndexes"
:foreigns="localKeyUsage"
:tab-uid="tabUid"
:conn-uid="connection.uid"
:index-types="workspace.indexTypes"
table="new"
:schema="schema"
mode="table"
@duplicate-field="duplicateField"
@remove-field="removeField"
@add-new-index="addNewIndex"
@add-to-index="addToIndex"
@rename-field="renameField"
/>
</div>
<WorkspaceTabPropsTableIndexesModal
v-if="isIndexesModal"
:local-indexes="localIndexes"
table="new"
:fields="localFields"
:index-types="workspace.indexTypes"
:workspace="workspace"
@hide="hideIndexesModal"
@indexes-update="indexesUpdate"
/>
<WorkspaceTabPropsTableForeignModal
v-if="isForeignModal"
:local-key-usage="localKeyUsage"
:connection="connection"
table="new"
:schema="schema"
:schema-tables="schemaTables"
:fields="localFields"
:workspace="workspace"
@hide="hideForeignModal"
@foreigns-update="foreignsUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabPropsTableFields from '@/components/WorkspaceTabPropsTableFields';
import WorkspaceTabPropsTableIndexesModal from '@/components/WorkspaceTabPropsTableIndexesModal';
import WorkspaceTabPropsTableForeignModal from '@/components/WorkspaceTabPropsTableForeignModal';
import WorkspaceTabNewTableEmptyState from '@/components/WorkspaceTabNewTableEmptyState';
export default {
name: 'WorkspaceTabNewTable',
components: {
BaseLoader,
WorkspaceTabPropsTableFields,
WorkspaceTabPropsTableIndexesModal,
WorkspaceTabPropsTableForeignModal,
WorkspaceTabNewTableEmptyState
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isIndexesModal: false,
isForeignModal: false,
isOptionsChanging: false,
originalFields: [],
localFields: [],
originalKeyUsage: [],
localKeyUsage: [],
originalIndexes: [],
localIndexes: [],
tableOptions: {},
localOptions: {},
lastTable: null,
newFieldsCounter: 0
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
tabUid () {
return this.$vnode.key;
},
defaultCollation () {
if (this.workspace.customizations.collations)
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
return '';
},
defaultEngine () {
if (this.workspace.customizations.engines)
return this.workspace.engines.find(engine => engine.isDefault).name;
return '';
},
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') : [];
},
isChanged () {
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) ||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) ||
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions);
}
},
watch: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.tableOptions = {
name: '',
type: 'table',
engine: this.defaultEngine,
comment: '',
collation: this.defaultCollation
};
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
renameTabs: 'workspaces/renameTabs',
removeTab: 'workspaces/removeTab',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
fields: this.localFields,
foreigns: this.localKeyUsage,
indexes: this.localIndexes,
options: this.localOptions
};
try {
const { status, response } = await Tables.createTable(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localOptions.name,
elementType: 'table',
type: 'table-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, table: this.localOptions.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
this.newFieldsCounter = 0;
},
clearChanges () {
this.localFields = JSON.parse(JSON.stringify(this.originalFields));
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes));
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
this.tableOptions = {
name: '',
type: 'table',
engine: this.defaultEngine,
comment: '',
collation: this.defaultCollation
};
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
this.newFieldsCounter = 0;
},
addField () {
this.localFields.push({
_antares_id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '',
type: this.workspace.dataTypes[0].types[0].name,
schema: this.schema,
numPrecision: null,
numLength: this.workspace.dataTypes[0].types[0].length,
datePrecision: null,
charLength: null,
nullable: false,
unsigned: false,
zerofill: false,
order: this.localFields.length + 1,
default: null,
charset: null,
collation: null,
autoIncrement: false,
onUpdate: '',
comment: ''
});
setTimeout(() => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper;
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;
});
},
duplicateField (uid) {
const fieldToClone = Object.assign({}, this.localFields.find(field => field._antares_id === uid));
fieldToClone._antares_id = uidGen();
fieldToClone.name = `${fieldToClone.name}_copy`;
fieldToClone.order = this.localFields.length + 1;
this.localFields = [...this.localFields, fieldToClone];
setTimeout(() => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20);
},
removeField (uid) {
this.localFields = this.localFields.filter(field => field._antares_id !== uid);
},
addNewIndex (payload) {
this.localIndexes = [...this.localIndexes, {
_antares_id: uidGen(),
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
fields: [payload.field],
type: payload.index,
comment: '',
indexType: 'BTREE',
indexComment: '',
cardinality: 0
}];
},
addToIndex (payload) {
this.localIndexes = this.localIndexes.map(index => {
if (index._antares_id === payload.index) index.fields.push(payload.field);
return index;
});
},
optionsUpdate (options) {
this.localOptions = options;
},
showIntdexesModal () {
this.isIndexesModal = true;
},
hideIndexesModal () {
this.isIndexesModal = false;
},
indexesUpdate (indexes) {
this.localIndexes = indexes;
},
showForeignModal () {
this.isForeignModal = true;
},
hideForeignModal () {
this.isForeignModal = false;
},
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();
}
}
}
}
};
</script>

View File

@@ -0,0 +1,33 @@
<template>
<div class="column col-12 empty">
<p class="h6 empty-subtitle">
{{ $t('message.thereAreNoTableFields') }}
</p>
<div class="empty-action">
<button class="btn btn-gray d-flex" @click="$emit('new-field')">
<i class="mdi mdi-24px mdi-playlist-plus mr-2" />
{{ $t('message.addNewField') }}
</button>
</div>
</div>
</template>
<script>
export default {
name: 'WorkspaceTabNewTableEmptyState'
};
</script>
<style scoped>
.empty {
border-radius: 0;
background: transparent;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-left: auto;
margin-right: auto;
z-index: 9;
}
</style>

View File

@@ -0,0 +1,323 @@
<template>
<div v-show="isSelected" 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>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localTrigger.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localTrigger.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalTrigger.definer">
{{ originalTrigger.definer.replaceAll('`', '') }}
</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>
<fieldset class="column columns mb-0" :disabled="customizations.triggerOnlyRename">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label>
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label>
<div class="input-group">
<select v-model="localTrigger.activation" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<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>
</fieldset>
</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.triggerStatement') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localTrigger.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script>
import QueryEditor from '@/components/QueryEditor';
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import Triggers from '@/ipc-api/Triggers';
export default {
name: 'WorkspaceTabNewTrigger',
components: {
BaseLoader,
QueryEditor
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
originalTrigger: {},
localTrigger: {},
lastTrigger: null,
sqlProxy: '',
editorHeight: 300,
localEvents: { INSERT: false, UPDATE: false, DELETE: false }
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
tabUid () {
return this.$vnode.key;
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalTrigger) !== JSON.stringify(this.localTrigger);
},
isDefinerInUsers () {
return this.originalTrigger ? this.workspace.users.some(user => this.originalTrigger.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: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalTrigger = {
sql: this.customizations.triggerSql,
definer: '',
table: this.schemaTables.length ? this.schemaTables[0].name : null,
activation: 'BEFORE',
event: this.customizations.triggerMultipleEvents ? ['INSERT'] : 'INSERT',
name: ''
};
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
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;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localTrigger
};
try {
const { status, response } = await Triggers.createTrigger(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localTrigger.name,
elementType: 'trigger',
type: 'trigger-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, trigger: this.localTrigger.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
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) {
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();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

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