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

Compare commits

...

174 Commits

Author SHA1 Message Date
020ce36312 chore(release): 0.4.2 2022-01-10 08:50:00 +01:00
b4545b178f feat(UI): textarea autofocus selecting a query tab, closes #166 2022-01-09 12:28:01 +01:00
d9a3eab015 perf(MySQL): support to ANSI_QUOTES sql_mode, closes #158 2022-01-05 18:23:31 +01:00
2ab49c218d Merge pull request #164 from toriphes/feat/keep-window-state
feat: keep window state
2021-12-28 19:30:51 +01:00
Giulio Ganci
8f9385d508 feat: save window state
open the main window in the last used position of the screen
2021-12-28 17:12:10 +01:00
0c002918eb feat(PostgreSQL): ability to cancel queries 2021-12-26 21:13:02 +01:00
3d56ec7b49 Merge pull request #157 from Fabio286/dependabot/npm_and_yarn/eslint-plugin-promise-6.0.0
build(deps-dev): bump eslint-plugin-promise from 5.2.0 to 6.0.0
2021-12-24 18:04:58 +01:00
48c3e6afc4 perf: hash for foreign key default names 2021-12-23 11:47:17 +01:00
dependabot[bot]
50a5c8fe1e build(deps-dev): bump eslint-plugin-promise from 5.2.0 to 6.0.0
Bumps [eslint-plugin-promise](https://github.com/xjamundx/eslint-plugin-promise) from 5.2.0 to 6.0.0.
- [Release notes](https://github.com/xjamundx/eslint-plugin-promise/releases)
- [Changelog](https://github.com/xjamundx/eslint-plugin-promise/blob/development/CHANGELOG.md)
- [Commits](https://github.com/xjamundx/eslint-plugin-promise/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-20 19:03:06 +00:00
b0d1f115c9 ci: update actions/checkout to v2 2021-12-20 09:27:36 +01:00
a59f77f618 feat(MySQL): ability to cancel queries 2021-12-19 11:59:09 +01:00
e7a1858091 fix(SQLite): exception with some fields 2021-12-16 09:16:15 +01:00
648a51efe8 Merge pull request #154 from Fabio286/all-contributors/add-wenj91
docs: add wenj91 as a contributor for code
2021-12-14 09:49:24 +01:00
38962c4807 Merge pull request #153 from wenj91/master
[TypeError: Cannot read properties of undefined (reading 'type') #152] bugfix
2021-12-14 09:48:58 +01:00
allcontributors[bot]
43e4cdd4cf docs: update .all-contributorsrc [skip ci] 2021-12-14 08:48:00 +00:00
allcontributors[bot]
91046c1ac1 docs: update README.md [skip ci] 2021-12-14 08:47:59 +00:00
文杰
63f8b9b6a1 Merge branch 'Fabio286:master' into master 2021-12-14 09:40:42 +08:00
文杰
ed476d9b5c Merge pull request #1 from wenj91:152-bugfix
[TypeError: Cannot read properties of undefined (reading 'type') #152] bugfix
2021-12-14 09:40:02 +08:00
文杰
f41d8c0480 [TypeError: Cannot read properties of undefined (reading 'type') #152] bugfix 2021-12-14 01:37:59 +00:00
e3b54a8be1 Merge pull request #151 from datlechin/master
Update vi-VN translation
2021-12-13 08:08:30 +01:00
Ngo Quoc Dat
c2c0394624 Update vi-VN translation 2021-12-13 08:48:07 +07:00
8a6f5eac59 chore(release): 0.4.1 2021-12-11 10:28:29 +01:00
a5fdcc1a85 feat: language format detection for text fields 2021-12-10 23:30:03 +01:00
1df21da47c refactor: moved to new vue slots API 2021-12-10 17:34:44 +01:00
8da0224876 fix(MySQL): wrong datetime fields default in table filler in some cases 2021-12-09 18:26:59 +01:00
359e14a9eb fix(MySQL): wrong value for fields "on update" in some conditions 2021-12-09 12:22:38 +01:00
813aa320d9 perf(UI): avoid columns size change when editing cells or scrolling results 2021-12-08 11:19:10 +01:00
aaa5549609 fix: cell disappear on edit in one column tables 2021-12-08 10:37:23 +01:00
35cb7e1dc4 fix: select all rows with ctrl+a when editing a cell 2021-12-08 10:09:01 +01:00
992a033cb2 fix: false positive with Windows Defender 2021-12-01 09:23:40 +01:00
5267b37eaf chore: updated appx icons 2021-11-28 16:28:54 +01:00
8fe30d8b6d ci: run tests before build 2021-11-25 21:37:48 +01:00
37ccb6b00d test: basic e2e tests 2021-11-25 18:34:33 +01:00
e8af2d24a8 perf(UI): disable save button in table creation when no fields are added 2021-11-25 17:23:46 +01:00
d7f1aa97af fix(SQLite): update rows with a text primary key 2021-11-25 16:25:40 +01:00
c46224635a chore(release): 0.4.0 2021-11-24 17:27:51 +01:00
cc99491fe4 fix(UI): notifications timeout anomalies 2021-11-24 16:59:07 +01:00
fe8435531e Merge pull request #143 from Fabio286/feat/read-only-mode-on-connections
feat: read-only mode
2021-11-24 14:41:31 +01:00
5d48fe08c7 feat(PostgreSQL): read-only mode 2021-11-24 14:24:52 +01:00
4437d44486 feat(MySQL): read-only mode 2021-11-24 13:04:14 +01:00
9fe3680bbb perf: update italian traslation 2021-11-24 09:52:10 +01:00
da1947e4ef fix(UI): hide tools menu if no tools available 2021-11-24 09:19:42 +01:00
37a848df9d Merge pull request #142 from Fabio286/dependabot/npm_and_yarn/electron-16.0.1
build(deps-dev): bump electron from 15.3.2 to 16.0.1
2021-11-23 17:38:21 +01:00
dependabot[bot]
9816965e18 build(deps-dev): bump electron from 15.3.2 to 16.0.1
Bumps [electron](https://github.com/electron/electron) from 15.3.2 to 16.0.1.
- [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/v15.3.2...v16.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-22 19:05:55 +00:00
98165cacaa fix(SQLite): hide schema creation 2021-11-19 16:50:15 +01:00
7ad3096b4e chore: update README.md 2021-11-19 16:14:11 +01:00
7d345cf795 Merge pull request #141 from Fabio286/feat/sqlite-implementation
feat:sqlite implementation
2021-11-19 16:11:00 +01:00
f40e9c592e feat(SQLite): triggers management 2021-11-19 15:36:07 +01:00
7671c585f5 feat(SQLite): views management 2021-11-19 13:13:35 +01:00
93b4a7063b perf(SQLite): improvements in field length detection 2021-11-18 19:43:08 +01:00
3efeb45c46 feat(SQLite): tables management 2021-11-18 11:36:46 +01:00
3fc227d2de feat(SQLite): readonly mode 2021-11-16 13:21:33 +01:00
604b371920 feat(SQLite): cell update in data tabs 2021-11-16 12:56:03 +01:00
fd321beece feat(SQLite): keys support 2021-11-16 12:27:51 +01:00
9fe4e6b9e3 Merge pull request #140 from Fabio286/all-contributors/add-kilianstallz
docs: add kilianstallz as a contributor for code
2021-11-15 22:38:24 +01:00
allcontributors[bot]
57bf90481b docs: update .all-contributorsrc [skip ci] 2021-11-15 21:37:49 +00:00
allcontributors[bot]
6062a32c1c docs: update README.md [skip ci] 2021-11-15 21:37:48 +00:00
94c899eb82 perf(SQLite): improvements in data visualization 2021-11-15 18:09:34 +01:00
44a4ca75bd Merge pull request #139 from kilianstallz/kilianstallz-patch-table-rows
Hotfix quote style on delete-table-rows handler
2021-11-15 17:01:10 +01:00
Kilian Stallinger
c8e1605b08 Hotfix quote style on delete-table-rows handler
Use single quote as intended for ID in delete query.
2021-11-15 16:21:09 +01:00
60e5556a3e chore(release): 0.3.9 2021-11-14 10:51:45 +01:00
f2fcc98839 feat(SQLite): table data visualization 2021-11-13 23:00:53 +01:00
c54438d6d3 feat(SQLite): connection add/edit masks 2021-11-13 11:34:30 +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
151 changed files with 9011 additions and 3232 deletions

136
.all-contributorsrc Normal file
View File

@@ -0,0 +1,136 @@
{
"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"
]
},
{
"login": "kilianstallz",
"name": "Kilian Stallinger",
"avatar_url": "https://avatars.githubusercontent.com/u/5290318?v=4",
"profile": "https://kilianstallinger.com",
"contributions": [
"code"
]
},
{
"login": "wenj91",
"name": "文杰",
"avatar_url": "https://avatars.githubusercontent.com/u/12549338?v=4",
"profile": "https://github.com/wenj91",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
}

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

@@ -12,13 +12,19 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:

View File

@@ -12,13 +12,19 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:

View File

@@ -12,13 +12,19 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm i
- name: Run tests
run: npm run test
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:

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

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

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

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

View File

@@ -2,6 +2,211 @@
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.4.2](https://github.com/Fabio286/antares/compare/v0.4.1...v0.4.2) (2022-01-10)
### Features
* **MySQL:** ability to cancel queries ([a59f77f](https://github.com/Fabio286/antares/commit/a59f77f618aea6156fc80fb832d3efcb9848411f))
* **PostgreSQL:** ability to cancel queries ([0c00291](https://github.com/Fabio286/antares/commit/0c002918eb0226f6b3f21ed62117495f86396fb1))
* save window state ([8f9385d](https://github.com/Fabio286/antares/commit/8f9385d50815635d091758ecd5d00884e3297ca0))
* **UI:** textarea autofocus selecting a query tab, closes [#166](https://github.com/Fabio286/antares/issues/166) ([b4545b1](https://github.com/Fabio286/antares/commit/b4545b178f795712c781a3f4fc35eec31b5ad902))
### Bug Fixes
* **SQLite:** exception with some fields ([e7a1858](https://github.com/Fabio286/antares/commit/e7a18580915e7739bfa97948c6a0c4fc90a7e78a))
### Improvements
* hash for foreign key default names ([48c3e6a](https://github.com/Fabio286/antares/commit/48c3e6afc43c51f70a16703f1a71194f43da7a3e))
* **MySQL:** support to ANSI_QUOTES sql_mode, closes [#158](https://github.com/Fabio286/antares/issues/158) ([d9a3eab](https://github.com/Fabio286/antares/commit/d9a3eab015302e9f23112f659658073ab3242191))
### [0.4.1](https://github.com/Fabio286/antares/compare/v0.4.0...v0.4.1) (2021-12-11)
### Features
* language format detection for text fields ([a5fdcc1](https://github.com/Fabio286/antares/commit/a5fdcc1a85aa188ff1b9a15b1a768aced026f360))
### Bug Fixes
* cell disappear on edit in one column tables ([aaa5549](https://github.com/Fabio286/antares/commit/aaa5549609664665bd4513632d621cb249b379c1))
* false positive with Windows Defender ([992a033](https://github.com/Fabio286/antares/commit/992a033cb2bede3d1eb52e19482d810f6692de1e))
* **MySQL:** wrong datetime fields default in table filler in some cases ([8da0224](https://github.com/Fabio286/antares/commit/8da022487650039b7f34a9c86a7bd9045eba65e2))
* **MySQL:** wrong value for fields "on update" in some conditions ([359e14a](https://github.com/Fabio286/antares/commit/359e14a9ebd48f86069ba7762fe00a7056f52d47))
* select all rows with ctrl+a when editing a cell ([35cb7e1](https://github.com/Fabio286/antares/commit/35cb7e1dc48d3a74e9d106cb1a37f454c1b4a4d1))
* **SQLite:** update rows with a text primary key ([d7f1aa9](https://github.com/Fabio286/antares/commit/d7f1aa97af32a4c51fc7022498bd47e15fa08430))
### Improvements
* **UI:** avoid columns size change when editing cells or scrolling results ([813aa32](https://github.com/Fabio286/antares/commit/813aa320d9ab799efea38a7110b7c0bdf7549123))
* **UI:** disable save button in table creation when no fields are added ([e8af2d2](https://github.com/Fabio286/antares/commit/e8af2d24a869f7667c069936648808952d2062ab))
## [0.4.0](https://github.com/Fabio286/antares/compare/v0.3.9...v0.4.0) (2021-11-24)
### Features
* **MySQL:** read-only mode ([4437d44](https://github.com/Fabio286/antares/commit/4437d44486c4f20b0bec4bf89d56016b08e36e79))
* **PostgreSQL:** read-only mode ([5d48fe0](https://github.com/Fabio286/antares/commit/5d48fe08c77755ed18b3f7a9ea834268e317e7ef))
* **SQLite:** cell update in data tabs ([604b371](https://github.com/Fabio286/antares/commit/604b3719204f7473ce4846624f08f8be9eec8b8f))
* **SQLite:** connection add/edit masks ([c54438d](https://github.com/Fabio286/antares/commit/c54438d6d3bad38bc76dfcd61f58929fe30279cb))
* **SQLite:** keys support ([fd321be](https://github.com/Fabio286/antares/commit/fd321beece075d3ad23fdd8541f9beb5727045a5))
* **SQLite:** readonly mode ([3fc227d](https://github.com/Fabio286/antares/commit/3fc227d2de53aae115226ad3c965bfb6e9f3eca6))
* **SQLite:** table data visualization ([f2fcc98](https://github.com/Fabio286/antares/commit/f2fcc9883972402eab4d51ef2a9796638dde2d3d))
* **SQLite:** tables management ([3efeb45](https://github.com/Fabio286/antares/commit/3efeb45c460f178b794de72367f8d542fd8ddd56))
* **SQLite:** triggers management ([f40e9c5](https://github.com/Fabio286/antares/commit/f40e9c592eeffd204aba21a0a0767a0c523fca49))
* **SQLite:** views management ([7671c58](https://github.com/Fabio286/antares/commit/7671c585f5f8049bd863db190d4fc60d8f0c6c66))
### Bug Fixes
* **SQLite:** hide schema creation ([98165ca](https://github.com/Fabio286/antares/commit/98165cacaa158c85ead0490d3caf579e2a17319f))
* **UI:** hide tools menu if no tools available ([da1947e](https://github.com/Fabio286/antares/commit/da1947e4efa7f0a26d6a231fadf750be055fbdd5))
* **UI:** notifications timeout anomalies ([cc99491](https://github.com/Fabio286/antares/commit/cc99491fe4a15812368f6c928b8c7801d7b255aa))
### Improvements
* **SQLite:** improvements in data visualization ([94c899e](https://github.com/Fabio286/antares/commit/94c899eb8288b41a5962ac3d24365227e1f9f485))
* **SQLite:** improvements in field length detection ([93b4a70](https://github.com/Fabio286/antares/commit/93b4a7063beeb5a7001cb06a74f05b23105212f5))
* update italian traslation ([9fe3680](https://github.com/Fabio286/antares/commit/9fe3680bbb17c192cffa85348e68794ab49beb81))
### [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)

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.

View File

@@ -1,15 +1,18 @@
<!-- 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.
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.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite.
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.
@@ -26,6 +29,7 @@ We are actively working on it, hoping to provide new cool features, improvements
- 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.
@@ -64,13 +68,12 @@ On macOS you can run `.dmg` distribution following [this guide](https://support.
This is a roadmap with major features will come in near future.
- Support for other databases.
- Database tools.
- Users management (add/edit/delete).
- Query history and bookmarks.
- More context menu shortcuts.
- More keyboard shortcuts.
- Import/export and migration.
- Support for other databases.
- Apple Silicon distribution
## Currently supported
@@ -79,7 +82,7 @@ This is a roadmap with major features will come in near future.
- [x] MySQL/MariaDB
- [x] PostgreSQL
- [ ] SQLite
- [x] SQLite
- [ ] MSSQL
- [ ] OracleDB
- [ ] More...
@@ -104,11 +107,36 @@ This is a roadmap with major features will come in near future.
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1)
## Translations
## Contributors ✨
**Italian Translation** / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
**Arabic Translation** (needs updates) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
**Spanish Translation** (needs updates) / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
**French Translation** (needs updates) / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
**Portugues (Brasil)** / [Daniel Eduardo](https://github.com/daeleduardo) [[#54](https://github.com/Fabio286/antares/pull/54)]
**Deutsch (Deutschland)** / [Christian Ratz](https://github.com/digitalgopnik) [[#74](https://github.com/Fabio286/antares/pull/74)]
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>
<td align="center"><a href="https://kilianstallinger.com"><img src="https://avatars.githubusercontent.com/u/5290318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kilian Stallinger</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=kilianstallz" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/wenj91"><img src="https://avatars.githubusercontent.com/u/12549338?v=4?s=100" width="100px;" alt=""/><br /><sub><b>文杰</b></sub></a><br /><a href="https://github.com/Fabio286/antares/commits?author=wenj91" title="Code">💻</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!

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
assets/appx/StoreLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

@@ -1,27 +1,46 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.3.2",
"description": "A cross-platform easy to use SQL client.",
"version": "0.4.2",
"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",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"test": "npm run lint",
"postinstall": "electron-builder install-app-deps",
"test": "npm run compile && node tests/app.spec.js",
"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,
"buildDependenciesFromSource": true,
"directories": {
"output": "build",
"buildResources": "assets"
},
"asarUnpack": "**\\*.{node,dll}",
"files": [
"dist/**/*",
"node_modules",
"package.json"
],
"win": {
"target": [
"nsis",
@@ -61,7 +80,9 @@
"artifactName": "${productName}-${version}-portable.exe"
},
"appx": {
"displayName": "Antares SQL Client",
"displayName": "Antares SQL",
"backgroundColor": "transparent",
"showNameOnTiles": true,
"identityName": "62514FabioDiStasio.AntaresSQLClient",
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
"applicationId": "FabioDiStasio.AntaresSQLClient"
@@ -81,55 +102,68 @@
]
}
},
"electronWebpack": {
"renderer": {
"webpackConfig": "webpack.config.js"
}
},
"dependencies": {
"@electron/remote": "^1.2.1",
"@mdi/font": "^5.9.55",
"ace-builds": "^1.4.12",
"@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95",
"@vscode/vscode-languagedetection": "^1.0.21",
"ace-builds": "^1.4.13",
"better-sqlite3": "^7.4.4",
"electron-log": "^4.4.1",
"electron-store": "^8.0.0",
"electron-store": "^8.0.1",
"electron-updater": "^4.3.9",
"electron-window-state": "^5.0.3",
"faker": "^5.5.3",
"marked": "^2.1.1",
"marked": "^4.0.0",
"moment": "^2.29.1",
"mysql2": "^2.3.0",
"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",
"ssh2-promise": "^0.2.0",
"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.15.0",
"@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": "^13.1.8",
"electron-builder": "^22.11.7",
"css-loader": "^6.5.0",
"electron": "^16.0.1",
"electron-builder": "^22.14.11",
"electron-devtools-installer": "^3.2.0",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.15.1",
"sass": "^1.37.5",
"sass-loader": "^10.2.0",
"eslint-plugin-promise": "^6.0.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",
"playwright": "^1.16.3",
"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.20.1",
"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": "^4.46.0"
"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

@@ -8,6 +8,10 @@ module.exports = {
collations: false,
engines: false,
connectionSchema: false,
sslConnection: false,
sshConnection: false,
fileConnection: false,
cancelQueries: false,
// Tools
processesList: false,
usersManagement: false,
@@ -22,6 +26,8 @@ module.exports = {
functions: false,
schedulers: false,
// Settings
elementsWrapper: '',
stringsWrapper: '"',
tableAdd: false,
viewAdd: false,
triggerAdd: false,
@@ -31,7 +37,11 @@ module.exports = {
schedulerAdd: false,
databaseEdit: false,
schemaEdit: false,
schemaDrop: false,
tableSettings: false,
tableOptions: false,
tableArray: false,
tableRealCount: false,
viewSettings: false,
triggerSettings: false,
triggerFunctionSettings: false,
@@ -43,13 +53,13 @@ module.exports = {
sortableFields: false,
unsigned: false,
nullable: false,
nullablePrimary: false,
zerofill: false,
autoIncrement: false,
comment: false,
collation: false,
definer: false,
onUpdate: false,
tableArray: false,
viewAlgorithm: false,
viewSqlSecurity: false,
viewUpdateOption: false,
@@ -69,8 +79,10 @@ module.exports = {
triggerTableInName: false,
triggerUpdateColumns: false,
triggerOnlyRename: false,
triggerEnableDisable: false,
triggerFunctionSql: false,
triggerFunctionlanguages: false,
parametersLength: false,
languages: false
languages: false,
readOnlyMode: false
};

View File

@@ -1,5 +1,6 @@
module.exports = {
maria: require('./mysql'),
mysql: require('./mysql'),
pg: require('./postgresql')
pg: require('./postgresql'),
sqlite: require('./sqlite')
};

View File

@@ -10,6 +10,9 @@ module.exports = {
connectionSchema: true,
collations: true,
engines: true,
sslConnection: true,
sshConnection: true,
cancelQueries: true,
// Tools
processesList: true,
// Structure
@@ -21,6 +24,8 @@ module.exports = {
functions: true,
schedulers: true,
// Settings
elementsWrapper: '',
stringsWrapper: '"',
tableAdd: true,
viewAdd: true,
triggerAdd: true,
@@ -28,6 +33,7 @@ module.exports = {
functionAdd: true,
schedulerAdd: true,
schemaEdit: true,
schemaDrop: true,
tableSettings: true,
viewSettings: true,
triggerSettings: true,
@@ -40,6 +46,7 @@ module.exports = {
unsigned: true,
nullable: true,
zerofill: true,
tableOptions: true,
autoIncrement: true,
comment: true,
collation: true,
@@ -56,5 +63,6 @@ module.exports = {
functionDeterministic: true,
functionDataAccess: true,
functionSql: 'BEGIN\r\n\r\nEND',
parametersLength: true
parametersLength: true,
readOnlyMode: true
};

View File

@@ -8,9 +8,13 @@ module.exports = {
defaultDatabase: 'postgres',
// Core
database: true,
sslConnection: true,
sshConnection: true,
cancelQueries: true,
// Tools
processesList: true,
// Structure
schemas: true,
tables: true,
views: true,
triggers: true,
@@ -18,12 +22,15 @@ module.exports = {
routines: true,
functions: true,
// Settings
elementsWrapper: '"',
stringsWrapper: '\'',
tableAdd: true,
viewAdd: true,
triggerAdd: true,
triggerFunctionAdd: true,
routineAdd: true,
functionAdd: true,
schemaDrop: true,
databaseEdit: false,
tableSettings: true,
viewSettings: true,
@@ -48,5 +55,7 @@ module.exports = {
triggerMultipleEvents: true,
triggerTableInName: true,
triggerOnlyRename: false,
languages: ['sql', 'plpgsql', 'c', 'internal']
triggerEnableDisable: true,
languages: ['sql', 'plpgsql', 'c', 'internal'],
readOnlyMode: true
};

View File

@@ -0,0 +1,27 @@
module.exports = {
// Core
fileConnection: true,
// Structure
schemas: false,
tables: true,
views: true,
triggers: true,
// Settings
elementsWrapper: '"',
stringsWrapper: '\'',
tableAdd: true,
viewAdd: true,
triggerAdd: true,
schemaEdit: false,
tableSettings: true,
tableRealCount: true,
viewSettings: true,
triggerSettings: true,
indexes: true,
foreigns: true,
sortableFields: true,
nullable: true,
nullablePrimary: true,
triggerSql: 'BEGIN\r\n\r\nEND',
readOnlyMode: true
};

View File

@@ -0,0 +1,137 @@
module.exports = [
{
group: 'integer',
types: [
{
name: 'INT',
length: true,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'INTEGER',
length: true,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'BIGINT',
length: true,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'NUMERIC',
length: true,
collation: false,
unsigned: true,
zerofill: true
},
{
name: 'BOOLEAN',
length: false,
collation: false,
unsigned: true,
zerofill: true
}
]
},
{
group: 'float',
types: [
{
name: 'FLOAT',
length: true,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'REAL',
length: true,
collation: false,
unsigned: false,
zerofill: false
}
]
},
{
group: 'string',
types: [
{
name: 'CHAR',
length: true,
collation: true,
unsigned: false,
zerofill: false
},
{
name: 'VARCHAR',
length: true,
collation: true,
unsigned: false,
zerofill: false
},
{
name: 'TEXT',
length: true,
collation: true,
unsigned: false,
zerofill: false
}
]
},
{
group: 'binary',
types: [
{
name: 'BLOB',
length: true,
collation: false,
unsigned: false,
zerofill: false
}
]
},
{
group: 'time',
types: [
{
name: 'DATE',
length: false,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'TIME',
length: true,
collation: false,
unsigned: false,
zerofill: false
},
{
name: 'DATETIME',
length: true,
collation: false,
unsigned: false,
zerofill: false
}
]
},
{
group: 'other',
types: [
{
name: 'NONE',
length: false,
collation: false,
unsigned: false,
zerofill: false
}
]
}
];

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

@@ -0,0 +1,5 @@
module.exports = [
'PRIMARY',
'INDEX',
'UNIQUE'
];

View File

@@ -1,103 +0,0 @@
'use strict';
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
import * as path from 'path';
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, VUEJS3_DEVTOOLS } = require('electron-devtools-installer');
// const oldDevToolsID = session.defaultSession.getAllExtensions().find(ext => ext.name === 'Vue.js devtools').id;
// session.defaultSession.removeExtension(oldDevToolsID);
// const toolName = await installExtension(VUEJS3_DEVTOOLS);
// console.log(toolName, 'installed');
}
else
await window.loadURL(new URL(`file:///${path.join(__dirname, 'index.html')}`).href);
}
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();
Menu.setApplicationMenu(null);
if (isDevelopment)
mainWindow.webContents.openDevTools();
});
}

View File

@@ -9,12 +9,16 @@ export default connections => {
port: +conn.port,
user: conn.user,
password: conn.password,
application_name: 'Antares SQL'
application_name: 'Antares SQL',
readonly: conn.readonly
};
if (conn.database)
params.database = conn.database;
if (conn.databasePath)
params.databasePath = conn.databasePath;
if (conn.ssl) {
params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null,
@@ -30,7 +34,8 @@ export default connections => {
username: conn.sshUser,
password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
passphrase: conn.sshPassphrase
};
}
@@ -47,7 +52,7 @@ export default connections => {
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err };
return { status: 'error', response: err.toString() };
}
});
@@ -61,12 +66,16 @@ export default connections => {
port: +conn.port,
user: conn.user,
password: conn.password,
application_name: 'Antares SQL'
application_name: 'Antares SQL',
readonly: conn.readonly
};
if (conn.database)
params.database = conn.database;
if (conn.databasePath)
params.databasePath = conn.databasePath;
if (conn.schema)
params.schema = conn.schema;
@@ -85,7 +94,8 @@ export default connections => {
username: conn.sshUser,
password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
passphrase: conn.sshPassphrase
};
}

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;
@@ -124,7 +135,7 @@ export default connections => {
}
});
ipcMain.handle('raw-query', async (event, { uid, query, schema }) => {
ipcMain.handle('raw-query', async (event, { uid, query, schema, tabUid }) => {
if (!query) return;
try {
@@ -132,6 +143,7 @@ export default connections => {
nest: true,
details: true,
schema,
tabUid,
comments: false
});
@@ -141,4 +153,16 @@ export default connections => {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('kill-tab-query', async (event, { uid, tabUid }) => {
if (!tabUid) return;
try {
await connections[uid].killTabQuery(tabUid);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -3,6 +3,7 @@ import faker from 'faker';
import moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import * as customizations from 'common/customizations';
import fs from 'fs';
export default (connections) => {
@@ -16,7 +17,7 @@ export default (connections) => {
}
});
ipcMain.handle('get-table-data', async (event, { uid, schema, table, limit, page, 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]
@@ -29,6 +30,9 @@ export default (connections) => {
if (sortParams && sortParams.field && sortParams.dir)
query.orderBy({ [sortParams.field]: sortParams.dir.toUpperCase() });
if (where)
query.where(where);
const result = await query.run({ details: true, schema });
return { status: 'success', response: result };
@@ -48,6 +52,16 @@ export default (connections) => {
}
});
ipcMain.handle('get-table-options', async (event, params) => {
try {
const result = await connections[params.uid].getTableOptions(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-table-indexes', async (event, params) => {
try {
const result = await connections[params.uid].getTableIndexes(params);
@@ -71,12 +85,13 @@ export default (connections) => {
});
ipcMain.handle('update-table-cell', async (event, params) => {
delete params.row._id;
delete params.row._antares_id;
const { stringsWrapper: sw } = customizations[connections[params.uid]._client];
try { // TODO: move to client classes
let escapedParam;
let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
const id = typeof params.id === 'number' ? params.id : `${sw}${params.id}${sw}`;
if ([...NUMBER, ...FLOAT].includes(params.type))
escapedParam = params.content;
@@ -89,6 +104,9 @@ export default (connections) => {
case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
case 'sqlite':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (ARRAY.includes(params.type))
@@ -109,6 +127,10 @@ export default (connections) => {
fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
case 'sqlite':
fileBlob = fs.readFileSync(params.content);
escapedParam = `X'${fileBlob.toString('hex')}'`;
break;
}
reload = true;
}
@@ -121,6 +143,9 @@ export default (connections) => {
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
case 'sqlite':
escapedParam = 'X\'\'';
break;
}
}
}
@@ -175,7 +200,7 @@ export default (connections) => {
const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
return typeof row[fieldName] === 'string'
? `"${row[fieldName]}"`
? `'${row[fieldName]}'`
: row[fieldName];
}).join(',');

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

View File

@@ -1,6 +1,13 @@
'use strict';
import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
import { SQLiteClient } from './clients/SQLiteClient';
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 {
/**
@@ -23,12 +30,16 @@ export class ClientsFactory {
* @memberof ClientsFactory
*/
static getConnection (args) {
args.logger = queryLogger;
switch (args.client) {
case 'mysql':
case 'maria':
return new MySQLClient(args);
case 'pg':
return new PostgreSQLClient(args);
case 'sqlite':
return new SQLiteClient(args);
default:
throw new Error(`Unknown database client: ${args.client}`);
}

View File

@@ -9,6 +9,7 @@ export class MySQLClient extends AntaresCore {
super(args);
this._schema = null;
this._runningConnections = new Map();
this.types = {
0: 'DECIMAL',
@@ -118,17 +119,35 @@ export class MySQLClient extends AntaresCore {
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) {
this._ssh = new SSH2Promise({ ...this._params.ssh });
try {
this._ssh = new SSH2Promise({ ...this._params.ssh });
this._tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = this._tunnel.localPort;
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)
if (!this._poolSize) {
this._connection = await mysql.createConnection(dbConfig);
// ANSI_QUOTES check
const res = await this.getVariable('sql_mode', 'global');
const sqlMode = res?.value.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (this._params.readonly)
await this.raw('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
await this.raw(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
}
else {
this._connection = mysql.createPool({
...dbConfig,
@@ -140,6 +159,22 @@ export class MySQLClient extends AntaresCore {
return next();
}
});
// ANSI_QUOTES check
const res = await this.getVariable('sql_mode', 'global');
const sqlMode = res?.value.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES');
if (hasAnsiQuotes)
await this._connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
this._connection.on('connection', connection => {
if (this._params.readonly)
connection.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`);
});
}
}
@@ -181,6 +216,7 @@ export class MySQLClient extends AntaresCore {
const tablesArr = [];
const triggersArr = [];
let schemaSize = 0;
for (const db of filteredDatabases) {
if (!schemas.has(db.Database)) continue;
@@ -218,6 +254,9 @@ export class MySQLClient extends AntaresCore {
break;
}
const tableSize = table.Data_length + table.Index_length;
schemaSize += tableSize;
return {
name: table.Name,
type: tableType,
@@ -226,7 +265,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
};
@@ -270,7 +309,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,
@@ -303,6 +342,7 @@ export class MySQLClient extends AntaresCore {
return {
name: db.Database,
size: schemaSize,
tables: remappedTables,
functions: remappedFunctions,
procedures: remappedProcedures,
@@ -313,6 +353,7 @@ export class MySQLClient extends AntaresCore {
else {
return {
name: db.Database,
size: 0,
tables: [],
functions: [],
procedures: [],
@@ -402,7 +443,7 @@ export class MySQLClient extends AntaresCore {
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: remappedFields ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE,
type: (remappedFields && remappedFields[field.COLUMN_NAME]) ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE,
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION,
@@ -414,11 +455,13 @@ export class MySQLClient extends AntaresCore {
unsigned: field.COLUMN_TYPE.includes('unsigned'),
zerofill: field.COLUMN_TYPE.includes('zerofill'),
order: field.ORDINAL_POSITION,
default: remappedFields ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT,
default: (remappedFields && remappedFields[field.COLUMN_NAME]) ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT,
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment'),
onUpdate: field.EXTRA.toLowerCase().includes('on update') ? field.EXTRA.replace('on update', '') : '',
onUpdate: field.EXTRA.toLowerCase().includes('on update')
? field.EXTRA.substr(field.EXTRA.indexOf('on update') + 9, field.EXTRA.length).trim()
: '',
comment: field.COLUMN_COMMENT
};
});
@@ -437,6 +480,43 @@ export class MySQLClient extends AntaresCore {
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
@@ -522,7 +602,7 @@ export class MySQLClient extends AntaresCore {
/**
* CREATE DATABASE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async createSchema (params) {
@@ -532,7 +612,7 @@ export class MySQLClient extends AntaresCore {
/**
* ALTER DATABASE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async alterSchema (params) {
@@ -542,7 +622,7 @@ export class MySQLClient extends AntaresCore {
/**
* DROP DATABASE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async dropSchema (params) {
@@ -582,7 +662,7 @@ export class MySQLClient extends AntaresCore {
/**
* DROP VIEW
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async dropView (params) {
@@ -593,12 +673,17 @@ export class MySQLClient extends AntaresCore {
/**
* ALTER VIEW
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async alterView (params) {
const { view } = params;
let sql = `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` : ''}`;
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 \`${view.schema}\`.\`${view.oldName}\` TO \`${view.schema}\`.\`${view.name}\``;
@@ -609,7 +694,7 @@ export class MySQLClient extends AntaresCore {
/**
* CREATE VIEW
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async createView (params) {
@@ -642,7 +727,7 @@ export class MySQLClient extends AntaresCore {
/**
* DROP TRIGGER
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async dropTrigger (params) {
@@ -653,7 +738,7 @@ export class MySQLClient extends AntaresCore {
/**
* ALTER TRIGGER
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async alterTrigger (params) {
@@ -675,7 +760,7 @@ export class MySQLClient extends AntaresCore {
/**
* CREATE TRIGGER
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async createTrigger (params) {
@@ -749,7 +834,7 @@ export class MySQLClient extends AntaresCore {
/**
* DROP PROCEDURE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async dropRoutine (params) {
@@ -760,7 +845,7 @@ export class MySQLClient extends AntaresCore {
/**
* ALTER PROCEDURE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async alterRoutine (params) {
@@ -782,7 +867,7 @@ export class MySQLClient extends AntaresCore {
/**
* CREATE PROCEDURE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async createRoutine (params) {
@@ -876,7 +961,7 @@ export class MySQLClient extends AntaresCore {
/**
* DROP FUNCTION
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async dropFunction (params) {
@@ -887,7 +972,7 @@ export class MySQLClient extends AntaresCore {
/**
* ALTER FUNCTION
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async alterFunction (params) {
@@ -909,14 +994,16 @@ export class MySQLClient extends AntaresCore {
/**
* CREATE FUNCTION
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async createFunction (params) {
const parameters = params.parameters.reduce((acc, curr) => {
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
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 = params.returns ? params.sql : 'BEGIN\n RETURN 0;\nEND';
@@ -968,7 +1055,7 @@ export class MySQLClient extends AntaresCore {
/**
* DROP EVENT
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async dropEvent (params) {
@@ -979,7 +1066,7 @@ export class MySQLClient extends AntaresCore {
/**
* ALTER EVENT
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async alterEvent (params) {
@@ -1005,7 +1092,7 @@ export class MySQLClient extends AntaresCore {
/**
* CREATE EVENT
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async createEvent (params) {
@@ -1022,6 +1109,16 @@ export class MySQLClient extends AntaresCore {
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
*
@@ -1061,6 +1158,26 @@ export class MySQLClient extends AntaresCore {
});
}
/**
* SHOW VARIABLES LIKE %variable%
*
* @param {String} variable
* @param {'global'|'session'|null} level
* @returns {Object} variable
* @memberof MySQLClient
*/
async getVariable (variable, level) {
const sql = `SHOW${level ? ' ' + level.toUpperCase() : ''} VARIABLES LIKE '%${variable}%'`;
const results = await this.raw(sql);
if (results.rows.length) {
return {
name: results.rows[0].Variable_name,
value: results.rows[0].Value
};
}
}
/**
* SHOW ENGINES
*
@@ -1132,14 +1249,84 @@ export class MySQLClient extends AntaresCore {
});
}
/**
*
* @param {number} id
* @returns {Promise<null>}
*/
async killProcess (id) {
return await this.raw(`KILL ${id}`);
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async killTabQuery (tabUid) {
const id = this._runningConnections.get(tabUid);
if (id)
return await this.killProcess(id);
}
/**
* CREATE TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async createTable (params) {
const sql = `CREATE TABLE \`${params.schema}\`.\`${params.name}\` (\`${params.name}_ID\` INT NULL) COMMENT='${params.comment}', COLLATE='${params.collation}', ENGINE=${params.engine}`;
const {
schema,
fields,
foreigns,
indexes,
options
} = params;
const newColumns = [];
const newIndexes = [];
const newForeigns = [];
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);
}
@@ -1147,7 +1334,7 @@ export class MySQLClient extends AntaresCore {
/**
* ALTER TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async alterTable (params) {
@@ -1282,7 +1469,7 @@ export class MySQLClient extends AntaresCore {
/**
* DUPLICATE TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async duplicateTable (params) {
@@ -1293,7 +1480,7 @@ export class MySQLClient extends AntaresCore {
/**
* TRUNCATE TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async truncateTable (params) {
@@ -1304,7 +1491,7 @@ export class MySQLClient extends AntaresCore {
/**
* DROP TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async dropTable (params) {
@@ -1403,6 +1590,9 @@ export class MySQLClient extends AntaresCore {
const isPool = typeof this._connection.getConnection === 'function';
const connection = isPool ? await this._connection.getConnection() : this._connection;
if (args.tabUid && isPool)
this._runningConnections.set(args.tabUid, connection.connection.connectionId);
if (args.schema)
await connection.query(`USE \`${args.schema}\``);
@@ -1463,7 +1653,10 @@ export class MySQLClient extends AntaresCore {
});
}
catch (err) {
if (isPool) connection.release();
if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
@@ -1472,7 +1665,10 @@ export class MySQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
if (isPool) connection.release();
if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
}
@@ -1487,7 +1683,10 @@ export class MySQLClient extends AntaresCore {
keys: keysArr
});
}).catch((err) => {
if (isPool) connection.release();
if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
});
});
@@ -1495,7 +1694,10 @@ export class MySQLClient extends AntaresCore {
resultsArr.push({ rows, report, fields, keys, duration });
}
if (isPool) connection.release();
if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
}

View File

@@ -9,6 +9,7 @@ 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
@@ -20,6 +21,7 @@ export class PostgreSQLClient extends AntaresCore {
super(args);
this._schema = null;
this._runningConnections = new Map();
this.types = {};
for (const key in types.builtins)
@@ -36,6 +38,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], [])
@@ -65,23 +87,38 @@ export class PostgreSQLClient extends AntaresCore {
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) {
this._ssh = new SSH2Promise({ ...this._params.ssh });
try {
this._ssh = new SSH2Promise({ ...this._params.ssh });
this._tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = this._tunnel.localPort;
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(dbConfig);
await client.connect();
this._connection = client;
if (this._params.readonly)
await this.raw('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
}
else {
const pool = new Pool({ ...dbConfig, max: this._poolSize });
this._connection = pool;
if (this._params.readonly) {
this._connection.on('connect', connection => {
connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
});
}
}
}
@@ -94,7 +131,7 @@ export class PostgreSQLClient extends AntaresCore {
}
/**
* Executes an USE query
* Executes an "USE" query
*
* @param {String} schema
* @memberof PostgreSQLClient
@@ -102,7 +139,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}"`);
}
/**
@@ -117,6 +154,7 @@ export class PostgreSQLClient extends AntaresCore {
const tablesArr = [];
const triggersArr = [];
let schemaSize = 0;
for (const db of databases) {
if (!schemas.has(db.database)) continue;
@@ -142,19 +180,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) {
@@ -170,11 +209,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: ''
@@ -213,17 +255,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,
@@ -235,6 +276,7 @@ export class PostgreSQLClient extends AntaresCore {
else {
return {
name: db.database,
size: 0,
tables: [],
functions: [],
procedures: [],
@@ -306,6 +348,40 @@ export class PostgreSQLClient extends AntaresCore {
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
@@ -440,7 +516,7 @@ export class PostgreSQLClient extends AntaresCore {
/**
* CREATE SCHEMA
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async createSchema (params) {
@@ -450,7 +526,7 @@ export class PostgreSQLClient extends AntaresCore {
/**
* ALTER DATABASE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async alterSchema (params) {
@@ -460,7 +536,7 @@ export class PostgreSQLClient extends AntaresCore {
/**
* DROP DATABASE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof MySQLClient
*/
async dropSchema (params) {
@@ -492,26 +568,26 @@ export class PostgreSQLClient extends AntaresCore {
/**
* DROP VIEW
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async dropView (params) {
const sql = `DROP VIEW ${params.schema}.${params.view}`;
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
/**
* ALTER VIEW
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async alterView (params) {
const { view } = params;
let sql = `CREATE OR REPLACE VIEW ${view.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 ${view.schema}.${view.oldName} RENAME TO ${view.name}`;
sql += `; ALTER VIEW "${view.schema}"."${view.oldName}" RENAME TO "${view.name}"`;
return await this.raw(sql);
}
@@ -519,11 +595,11 @@ export class PostgreSQLClient extends AntaresCore {
/**
* CREATE VIEW
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async createView (params) {
const sql = `CREATE VIEW ${params.schema}.${params.name} AS ${params.sql}`;
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql);
}
@@ -537,19 +613,25 @@ export class PostgreSQLClient extends AntaresCore {
const [table, triggerName] = trigger.split('.');
const results = await this.raw(`
SELECT event_object_schema AS table_schema,
event_object_table AS table_name,
trigger_schema,
trigger_name,
string_agg(event_manipulation, ',') AS event,
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
FROM information_schema.triggers
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
GROUP BY 1,2,3,4,6,7,8,9
ORDER BY table_schema,
table_name
`);
@@ -559,7 +641,7 @@ export class PostgreSQLClient extends AntaresCore {
sql: row.definition,
name: row.trigger_name,
table: row.table_name,
event: row.event.split(','),
event: [...new Set(row.event.split(','))],
activation: row.activation
};
})[0];
@@ -568,7 +650,7 @@ export class PostgreSQLClient extends AntaresCore {
/**
* DROP TRIGGER
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async dropTrigger (params) {
@@ -580,7 +662,7 @@ export class PostgreSQLClient extends AntaresCore {
/**
* ALTER TRIGGER
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async alterTrigger (params) {
@@ -602,7 +684,7 @@ export class PostgreSQLClient extends AntaresCore {
/**
* CREATE TRIGGER
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async createTrigger (params) {
@@ -611,6 +693,18 @@ export class PostgreSQLClient extends AntaresCore {
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 });
}
/**
* SHOW CREATE PROCEDURE
*
@@ -724,7 +818,7 @@ 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(',')
: '';
@@ -853,7 +947,7 @@ 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(',')
: '';
@@ -995,22 +1089,88 @@ export class PostgreSQLClient extends AntaresCore {
});
}
/**
*
* @param {number} id
* @returns {Promise<null>}
*/
async killProcess (id) {
return await this.raw(`SELECT pg_terminate_backend(${id})`);
}
/**
*
* @param {string} tabUid
* @returns {Promise<null>}
*/
async killTabQuery (tabUid) {
const id = this._runningConnections.get(tabUid);
if (id)
return await this.raw(`SELECT pg_cancel_backend(${id})`);
}
/**
* CREATE TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async createTable (params) {
const sql = `CREATE TABLE "${params.schema}"."${params.name}" (${params.name}_id INTEGER NULL); ALTER TABLE "${params.schema}"."${params.name}" DROP COLUMN ${params.name}_id`;
const {
schema,
fields,
foreigns,
indexes,
options
} = params;
const newColumns = [];
const newIndexes = [];
const manageIndexes = [];
const newForeigns = [];
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);
}
/**
* ALTER TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async alterTable (params) {
@@ -1034,45 +1194,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 "${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
@@ -1098,6 +1249,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}"`);
@@ -1115,39 +1267,39 @@ 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 "${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 "${schema}"."${table}" ${alterColumns.join(', ')}; `;
@@ -1164,33 +1316,33 @@ export class PostgreSQLClient extends AntaresCore {
/**
* DUPLICATE TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async duplicateTable (params) {
const sql = `CREATE TABLE ${params.schema}.${params.table}_copy (LIKE ${params.schema}.${params.table} INCLUDING ALL)`;
const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" (LIKE "${params.schema}"."${params.table}" INCLUDING ALL)`;
return await this.raw(sql);
}
/**
* TRUNCATE TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async truncateTable (params) {
const sql = `TRUNCATE TABLE ${params.schema}.${params.table}`;
const sql = `TRUNCATE TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* DROP TABLE
*
* @returns {Array.<Object>} parameters
* @returns {Promise<null>}
* @memberof PostgreSQLClient
*/
async dropTable (params) {
const sql = `DROP TABLE ${params.schema}.${params.table}`;
const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
@@ -1214,7 +1366,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, []);
@@ -1261,6 +1413,8 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async raw (sql, args) {
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
args = {
nest: false,
details: false,
@@ -1269,9 +1423,6 @@ export class PostgreSQLClient extends AntaresCore {
...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
@@ -1283,7 +1434,14 @@ export class PostgreSQLClient extends AntaresCore {
.map(q => q.trim())
: [sql];
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
const isPool = this._connection instanceof Pool;
const connection = isPool ? await this._connection.connect() : this._connection;
if (args.tabUid && isPool)
this._runningConnections.set(args.tabUid, connection.processID);
if (args.schema && args.schema !== 'public')
await this.use(args.schema);
for (const query of queries) {
if (!query) continue;
@@ -1293,15 +1451,12 @@ export class PostgreSQLClient extends AntaresCore {
let keysArr = [];
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
this._connection.query({
rowMode: args.nest ? 'array' : null,
text: query
}, async (err, res) => {
timeStop = new Date();
(async () => {
try {
const res = await connection.query({ rowMode: args.nest ? 'array' : null, text: query });
timeStop = new Date();
if (err)
reject(err);
else {
let ast;
try {
@@ -1390,6 +1545,10 @@ export class PostgreSQLClient extends AntaresCore {
});
}
catch (err) {
if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
@@ -1398,6 +1557,10 @@ export class PostgreSQLClient extends AntaresCore {
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
}
@@ -1412,12 +1575,24 @@ export class PostgreSQLClient extends AntaresCore {
keys: keysArr
});
}
});
catch (err) {
if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
reject(err);
}
})();
});
resultsArr.push({ rows, report, fields, keys, duration });
}
if (isPool) {
connection.release();
this._runningConnections.delete(args.tabUid);
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
}
}

View File

@@ -0,0 +1,806 @@
'use strict';
import sqlite from 'better-sqlite3';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/sqlite';
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
export class SQLiteClient extends AntaresCore {
constructor (args) {
super(args);
this._schema = null;
}
_getTypeInfo (type) {
return dataTypes
.reduce((acc, group) => [...acc, ...group.types], [])
.filter(_type => _type.name === type.toUpperCase())[0];
}
/**
* @memberof SQLiteClient
*/
async connect () {
this._connection = sqlite(this._params.databasePath, {
fileMustExist: true,
readonly: this._params.readonly
});
}
/**
* @memberof SQLiteClient
*/
destroy () {}
/**
* Executes an USE query
*
* @memberof SQLiteClient
*/
use () {}
/**
* @param {Array} schemas list
* @returns {Array.<Object>} databases scructure
* @memberof SQLiteClient
*/
async getStructure (schemas) {
const { rows: databases } = await this.raw('SELECT * FROM pragma_database_list');
const filteredDatabases = databases;
const tablesArr = [];
const triggersArr = [];
let schemaSize = 0;
for (const db of filteredDatabases) {
if (!schemas.has(db.name)) continue;
let { rows: tables } = await this.raw(`
SELECT *
FROM "${db.name}".sqlite_master
WHERE type IN ('table', 'view')
AND name NOT LIKE 'sqlite_%'
ORDER BY name
`);
if (tables.length) {
tables = tables.map(table => {
table.Db = db.name;
return table;
});
tablesArr.push(...tables);
}
let { rows: triggers } = await this.raw(`SELECT * FROM "${db.name}".sqlite_master WHERE type='trigger'`);
if (triggers.length) {
triggers = triggers.map(trigger => {
trigger.Db = db.name;
return trigger;
});
triggersArr.push(...triggers);
}
}
return filteredDatabases.map(db => {
if (schemas.has(db.name)) {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.name).map(table => {
const tableSize = 0;
schemaSize += tableSize;
return {
name: table.name,
type: table.type,
rows: false,
size: false
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.name).map(trigger => {
return {
name: trigger.name,
table: trigger.tbl_name
};
});
return {
name: db.name,
size: schemaSize,
tables: remappedTables,
functions: [],
procedures: [],
triggers: remappedTriggers,
schedulers: []
};
}
else {
return {
name: db.name,
size: 0,
tables: [],
functions: [],
procedures: [],
triggers: [],
schedulers: []
};
}
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table scructure
* @memberof SQLiteClient
*/
async getTableColumns ({ schema, table }) {
const { rows: fields } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}')`);
return fields.map(field => {
const [type, length] = field.type.includes('(')
? field.type.replace(')', '').split('(').map(el => {
if (!isNaN(el)) el = +el;
return el;
})
: [field.type, null];
return {
name: field.name,
key: null,
type: type.trim(),
schema: schema,
table: table,
numPrecision: [...NUMBER, ...FLOAT].includes(type) ? length : null,
datePrecision: null,
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
nullable: !field.notnull,
unsigned: null,
zerofill: null,
order: field.cid + 1,
default: field.dflt_value,
charset: null,
collation: null,
autoIncrement: false,
onUpdate: null,
comment: ''
};
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table row count
* @memberof SQLiteClient
*/
async getTableApproximateCount ({ schema, table }) {
const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`);
return rows.length ? rows[0].count : 0;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table options
* @memberof SQLiteClient
*/
async getTableOptions ({ schema, table }) {
return { name: table };
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table indexes
* @memberof SQLiteClient
*/
async getTableIndexes ({ schema, table }) {
const remappedIndexes = [];
const { rows: primaryKeys } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}') WHERE pk != 0`);
for (const key of primaryKeys) {
remappedIndexes.push({
name: 'PRIMARY',
column: key.name,
indexType: null,
type: 'PRIMARY',
cardinality: null,
comment: '',
indexComment: ''
});
}
const { rows: indexes } = await this.raw(`SELECT * FROM "${schema}".pragma_index_list('${table}');`);
for (const index of indexes) {
const { rows: details } = await this.raw(`SELECT * FROM "${schema}".pragma_index_info('${index.name}');`);
for (const detail of details) {
remappedIndexes.push({
name: index.name,
column: detail.name,
indexType: null,
type: index.unique === 1 ? 'UNIQUE' : 'INDEX',
cardinality: null,
comment: '',
indexComment: ''
});
}
}
return remappedIndexes;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table key usage
* @memberof SQLiteClient
*/
async getKeyUsage ({ schema, table }) {
const { rows } = await this.raw(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`);
return rows.map(field => {
return {
schema: schema,
table: table,
field: field.from,
position: field.id + 1,
constraintPosition: null,
constraintName: field.id,
refSchema: schema,
refTable: field.table,
refField: field.to,
onUpdate: field.on_update,
onDelete: field.on_delete
};
});
}
async getUsers () {}
/**
* SHOW CREATE VIEW
*
* @returns {Array.<Object>} view informations
* @memberof SQLiteClient
*/
async getViewInformations ({ schema, view }) {
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='view' AND name='${view}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
sql: row.sql.match(/(?<=AS ).*?$/gs)[0],
name: view
};
})[0];
}
/**
* DROP VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async dropView (params) {
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
/**
* ALTER VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async alterView (params) {
const { view } = params;
try {
await this.dropView({ schema: view.schema, view: view.oldName });
await this.createView(view);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE VIEW
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async createView (params) {
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql);
}
/**
* SHOW CREATE TRIGGER
*
* @returns {Array.<Object>} view informations
* @memberof SQLiteClient
*/
async getTriggerInformations ({ schema, trigger }) {
const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='trigger' AND name='${trigger}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
sql: row.sql.match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: trigger,
table: row.sql.match(/(?<=ON `).*?(?=`)/gs)[0],
activation: row.sql.match(/(BEFORE|AFTER)/gs)[0],
event: row.sql.match(/(INSERT|UPDATE|DELETE)/gs)[0]
};
})[0];
}
/**
* DROP TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async dropTrigger (params) {
const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``;
return await this.raw(sql);
}
/**
* ALTER TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
async alterTrigger (params) {
const { trigger } = params;
const tempTrigger = Object.assign({}, trigger);
tempTrigger.name = `Antares_${tempTrigger.name}_tmp`;
try {
await this.createTrigger(tempTrigger);
await this.dropTrigger({ schema: trigger.schema, trigger: tempTrigger.name });
await this.dropTrigger({ schema: trigger.schema, trigger: trigger.oldName });
await this.createTrigger(trigger);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE TRIGGER
*
* @returns {Array.<Object>} parameters
* @memberof SQLiteClient
*/
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 });
}
/**
* SHOW COLLATION
*
* @returns {Array.<Object>} collations list
* @memberof SQLiteClient
*/
async getCollations () {
return [];
}
/**
* SHOW VARIABLES
*
* @returns {Array.<Object>} variables list
* @memberof SQLiteClient
*/
async getVariables () {
return [];
}
/**
* SHOW ENGINES
*
* @returns {Array.<Object>} engines list
* @memberof SQLiteClient
*/
async getEngines () {
return {
name: 'SQLite',
support: 'YES',
comment: '',
isDefault: true
};
}
/**
* SHOW VARIABLES LIKE '%vers%'
*
* @returns {Array.<Object>} version parameters
* @memberof SQLiteClient
*/
async getVersion () {
const os = require('os');
const sql = 'SELECT sqlite_version() AS version';
const { rows } = await this.raw(sql);
return {
number: rows[0].version,
name: 'SQLite',
arch: process.arch,
os: `${os.type()} ${os.release()}`
};
}
async getProcesses () {}
async killProcess () {}
/**
* CREATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async createTable (params) {
const {
schema,
fields,
foreigns,
indexes,
options
} = params;
const newColumns = [];
const newIndexes = [];
const manageIndexes = [];
const newForeigns = [];
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 !== true ? `(${length})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
${field.autoIncrement ? 'AUTO_INCREMENT' : ''}
${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
manageIndexes.push(`CREATE ${type === 'UNIQUE' ? type : ''} INDEX "${index.name}" ON "${options.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(', ')})`;
if (manageIndexes.length) sql = `${sql}; ${manageIndexes.join(';')}`;
return await this.raw(sql);
}
/**
* ALTER TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async alterTable (params) {
try {
await this.raw('BEGIN TRANSACTION');
await this.raw('PRAGMA foreign_keys = 0');
const tmpName = `Antares_${params.table}_tmp`;
await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${params.table}"`);
await this.dropTable(params);
const createTableParams = {
schema: params.schema,
fields: params.tableStructure.fields,
foreigns: params.tableStructure.foreigns,
indexes: params.tableStructure.indexes.filter(index => !index.name.includes('sqlite_autoindex')),
options: { name: params.tableStructure.name }
};
await this.createTable(createTableParams);
const insertFields = createTableParams.fields
.filter(field => {
return (
params.additions.every(add => add.name !== field.name) &&
params.deletions.every(del => del.name !== field.name)
);
})
.reduce((acc, curr) => {
acc.push(`"${curr.name}"`);
return acc;
}, []);
const selectFields = insertFields.map(field => {
const renamedField = params.changes.find(change => `"${change.name}"` === field);
if (renamedField)
return `"${renamedField.orgName}"`;
return field;
});
await this.raw(`INSERT INTO "${createTableParams.options.name}" (${insertFields.join(',')}) SELECT ${selectFields.join(',')} FROM "${tmpName}"`);
await this.dropTable({ schema: params.schema, table: tmpName });
await this.raw('PRAGMA foreign_keys = 1');
await this.raw('COMMIT');
}
catch (err) {
await this.raw('ROLLBACK');
return Promise.reject(err);
}
}
/**
* DUPLICATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async duplicateTable (params) { // TODO: retrive table informations and create a copy
const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" AS SELECT * FROM "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* TRUNCATE TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async truncateTable (params) {
const sql = `DELETE FROM "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* DROP TABLE
*
* @returns {Promise<null>}
* @memberof SQLiteClient
*/
async dropTable (params) {
const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
/**
* @returns {String} SQL string
* @memberof SQLiteClient
*/
getSQL () {
// SELECT
const selectArray = this._query.select.reduce(this._reducer, []);
let selectRaw = '';
if (selectArray.length)
selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * ';
// FROM
let fromRaw = '';
if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from)
fromRaw = 'FROM';
else if (Object.keys(this._query.insert).length)
fromRaw = 'INTO';
fromRaw += this._query.from ? ` ${this._query.schema ? `"${this._query.schema}".` : ''}"${this._query.from}" ` : '';
// WHERE
const whereArray = this._query.where
.reduce(this._reducer, [])
?.map(clausole => clausole.replace('= null', 'IS NULL'));
const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : '';
// UPDATE
const updateArray = this._query.update.reduce(this._reducer, []);
const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : '';
// INSERT
let insertRaw = '';
if (this._query.insert.length) {
const fieldsList = Object.keys(this._query.insert[0]);
const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`);
insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `;
}
// GROUP BY
const groupByArray = this._query.groupBy.reduce(this._reducer, []);
const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : '';
// ORDER BY
const orderByArray = this._query.orderBy.reduce(this._reducer, []);
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
// LIMIT
const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
// 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}`;
}
/**
* @param {string} sql raw SQL query
* @param {object} args
* @param {boolean} args.nest
* @param {boolean} args.details
* @param {boolean} args.split
* @returns {Promise}
* @memberof SQLiteClient
*/
async raw (sql, args) {
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
args = {
nest: false,
details: false,
split: true,
comments: true,
...args
};
if (!args.comments)
sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments
const resultsArr = [];
let paramsArr = [];
const queries = args.split
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm)
.filter(Boolean)
.map(q => q.trim())
: [sql];
const connection = this._connection;
for (const query of queries) {
if (!query) continue;
const timeStart = new Date();
let timeStop;
const keysArr = [];
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
(async () => {
let queryResult;
let affectedRows;
let fields;
const detectedTypes = {};
try {
const stmt = connection.prepare(query);
if (stmt.reader) {
queryResult = stmt.all();
fields = stmt.columns();
if (queryResult.length) {
fields.forEach(field => {
detectedTypes[field.name] = typeof queryResult[0][field.name];
});
}
}
else {
const info = queryResult = stmt.run();
affectedRows = info.changes;
}
}
catch (err) {
reject(err);
}
timeStop = new Date();
let remappedFields = fields
? fields.map(field => {
let [parsedType, length] = field.type?.includes('(')
? field.type.replace(')', '').split('(').map(el => {
if (!isNaN(el))
el = +el;
else
el = el.trim();
return el;
})
: [field.type, null];
if ([...TIME, ...DATETIME].includes(parsedType)) {
const firstNotNull = queryResult.find(res => res[field.name] !== null);
if (firstNotNull && firstNotNull[field.name].includes('.'))
length = firstNotNull[field.name].split('.').pop().length;
}
return {
name: field.name,
alias: field.name,
orgName: field.column,
schema: field.database,
table: field.table,
tableAlias: field.table,
orgTable: field.table,
type: field.type !== null ? parsedType : detectedTypes[field.name],
length
};
}).filter(Boolean)
: [];
if (args.details) {
paramsArr = remappedFields.map(field => {
return {
table: field.table,
schema: field.schema
};
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
for (const paramObj of paramsArr) {
if (!paramObj.table || !paramObj.schema) continue;
try {
const indexes = await this.getTableIndexes(paramObj);
remappedFields = remappedFields.map(field => {
// const detailedField = columns.find(f => f.name === field.name);
const fieldIndex = indexes.find(i => i.column === field.name);
if (field.table === paramObj.table && field.schema === paramObj.schema) {
// if (detailedField) {
// const length = detailedField.numPrecision || detailedField.charLength || detailedField.datePrecision || null;
// field = { ...field, ...detailedField, length };
// }
if (fieldIndex) {
const key = fieldIndex.type === 'PRIMARY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'mul';
field = { ...field, key };
};
}
return field;
});
}
catch (err) {
reject(err);
}
}
}
resolve({
duration: timeStop - timeStart,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: affectedRows !== undefined ? { affectedRows } : null,
fields: remappedFields,
keys: keysArr
});
})();
});
resultsArr.push({ rows, report, fields, keys, duration });
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
}
}

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

@@ -0,0 +1,179 @@
'use strict';
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
import * as path from 'path';
import Store from 'electron-store';
import * as windowStateKeeper from 'electron-window-state';
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;
let mainWindowState;
async function createMainWindow () {
const icon = require('../renderer/images/logo-32.png');
const window = new BrowserWindow({
width: mainWindowState.width,
height: mainWindowState.height,
x: mainWindowState.x,
y: mainWindowState.y,
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'
});
mainWindowState.manage(window);
window.on('moved', saveWindowState);
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', () => {
window.removeListener('moved', saveWindowState);
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 () => {
mainWindowState = windowStateKeeper({
defaultWidth: 1024,
defaultHeight: 800
});
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);
}
function saveWindowState () {
mainWindowState.saveState(mainWindow);
}

View File

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

@@ -6,18 +6,18 @@
@confirm="runRoutine"
@hide="closeModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-play mr-1" />
<span class="cut-text">{{ $t('word.parameters') }}: {{ localRoutine.name }}</span>
</div>
</template>
<div slot="body">
<template #body>
<div class="content">
<form class="form-horizontal">
<div
v-for="(parameter, i) in inParameters"
:key="parameter._id"
:key="parameter._antares_id"
class="form-group"
>
<div class="col-4">
@@ -31,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>
@@ -39,7 +43,7 @@
</div>
</form>
</div>
</div>
</template>
</ConfirmModal>
</template>
@@ -127,4 +131,8 @@ export default {
.field-type {
font-size: 0.6rem;
}
.input-group-addon {
max-width: 100px;
}
</style>

View File

@@ -5,16 +5,16 @@
@confirm="$emit('confirm')"
@hide="$emit('close')"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ $t('message.unsavedChanges') }}
</div>
</template>
<div slot="body">
<template #body>
<div>
{{ $t('message.discardUnsavedChanges') }}
</div>
</div>
</template>
</ConfirmModal>
</template>

View File

@@ -41,7 +41,7 @@
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!field.autoIncrement"
:checked="!fieldsToExclude.includes(field.name)"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
@@ -251,7 +251,7 @@ export default {
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = Number.isNaN(+field.default) ? null : +field.default;
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('\'')
@@ -264,7 +264,7 @@ export default {
else if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
else if (DATETIME.includes(field.type)) {
if (field.default && ['current_timestamp', 'now()'].includes(field.default.toLowerCase())) {
if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
@@ -281,7 +281,7 @@ export default {
rowObj[field.name] = { value: fieldDefault };
if (field.autoIncrement)// Disable by default auto increment fields
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
}

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,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.functionSql)
this.localFunction.sql = this.customizations.functionSql;
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

@@ -13,7 +13,7 @@
</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>

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

@@ -1,183 +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 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="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.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>
</form>
<div v-if="customizations.triggerStatementInCreation" class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor
ref="queryEditor"
:value.sync="localTrigger.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import QueryEditor from '@/components/QueryEditor';
export default {
name: 'ModalNewTrigger',
components: {
ConfirmModal,
QueryEditor
},
props: {
workspace: Object
},
data () {
return {
localTrigger: {
definer: '',
sql: '',
name: '',
table: '',
activation: 'BEFORE',
event: 'INSERT'
},
isOptionsChanging: false,
localEvents: { INSERT: false, UPDATE: false, DELETE: false },
editorHeight: 150
};
},
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') : [];
},
customizations () {
return this.workspace.customizations;
}
},
created () {
this.localTrigger.table = this.schemaTables.length ? this.schemaTables[0].name : '';
this.localTrigger.sql = this.customizations.triggerSql;
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewTrigger () {
this.$emit('open-create-trigger-editor', this.localTrigger);
},
changeEvents (event) {
if (this.customizations.triggerMultipleEvents) {
this.localEvents[event] = !this.localEvents[event];
this.localTrigger.event = [];
for (const key in this.localEvents) {
if (this.localEvents[key])
this.localTrigger.event.push(key);
}
}
}
}
};
</script>

View File

@@ -1,132 +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.triggerFunctionlanguages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localFunction.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewTriggerFunction',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localFunction: {
definer: '',
sql: '',
name: '',
comment: '',
language: null
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
if (this.customizations.triggerFunctionlanguages)
this.localFunction.language = this.customizations.triggerFunctionlanguages[0];
if (this.customizations.triggerFunctionSql)
this.localFunction.sql = this.customizations.triggerFunctionSql;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewFunction () {
this.$emit('open-create-function-editor', this.localFunction);
}
}
};
</script>

View File

@@ -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,5 +1,15 @@
<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">
@@ -12,34 +22,54 @@
<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"
>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-1" />
<span>{{ $t('word.refresh') }}</span>
</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">
@@ -83,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"
/>
@@ -102,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
@@ -119,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: '',
@@ -245,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"
@@ -25,12 +22,12 @@
:hide-footer="true"
@hide="hideInfoModal"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
</div>
</template>
<div :slot="'body'">
<template #body>
<div>
<div>
<TextEditor
@@ -41,7 +38,7 @@
/>
</div>
</div>
</div>
</template>
</ConfirmModal>
</div>
</template>
@@ -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

@@ -184,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">
@@ -197,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">
@@ -280,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>
@@ -396,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);

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

@@ -15,16 +15,16 @@
@confirm="confirmDeleteConnection"
@hide="hideConfirmModal"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ $t('message.deleteConnection') }}
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} <b>{{ connectionName }}</b>?
</div>
</div>
</template>
</ConfirmModal>
</BaseContextMenu>
</template>

View File

@@ -40,15 +40,13 @@ export default {
}
},
watch: {
notifications: {
deep: true,
handler: function (notification) {
if (notification.length) {
this.timeouts[notification[0].uid] = setTimeout(() => {
this.removeNotification(notification[0].uid);
delete this.timeouts[notification.uid];
}, this.notificationsTimeout * 1000);
}
'notifications.length': function (val) {
if (val > 0) {
const nUid = this.notifications[0].uid;
this.timeouts[nUid] = setTimeout(() => {
this.removeNotification(nUid);
delete this.timeouts[nUid];
}, this.notificationsTimeout * 1000);
}
}
},
@@ -63,11 +61,14 @@ export default {
}
},
rearmTimeouts () {
const delay = 50;
let i = this.notifications.length * delay;
for (const notification of this.notifications) {
this.timeouts[notification.uid] = setTimeout(() => {
this.removeNotification(notification.uid);
delete this.timeouts[notification.uid];
}, this.notificationsTimeout * 1000);
}, (this.notificationsTimeout * 1000) + i);
i = i > delay ? i - delay : 0;
}
}
}

View File

@@ -6,12 +6,12 @@
:hide-footer="true"
@hide="hideScratchpad"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-notebook-edit-outline mr-1" /> {{ $t('word.scratchpad') }}
</div>
</template>
<div :slot="'body'">
<template #body>
<div>
<div>
<TextEditor
@@ -24,7 +24,7 @@
</div>
<small class="text-gray">{{ $t('message.markdownSupported') }}</small>
</div>
</div>
</template>
</ConfirmModal>
</template>

View File

@@ -92,7 +92,7 @@ export default {
}
},
hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
}
},
methods: {
@@ -200,7 +200,7 @@ export default {
}
&::before {
content: '';
content: "";
height: 0;
width: 3px;
transition: height 0.2s;

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

View File

@@ -18,7 +18,6 @@
<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}"
@@ -68,6 +67,23 @@
</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"
@@ -91,7 +107,7 @@
: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}`)}`">
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.view`)}`">
{{ tab.elementName | cutText }}
<span
class="btn btn-clear"
@@ -102,6 +118,108 @@
</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"
@@ -137,63 +255,70 @@
</span>
</a>
</li>
<li slot="header" class="tab-item dropdown tools-dropdown">
<a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
<template #header>
<li
v-if="workspace.customizations.processesList"
class="tab-item dropdown tools-dropdown"
>
<i class="mdi mdi-24px mdi-tools" />
</a>
<ul class="menu text-left text-uppercase">
<li v-if="workspace.customizations.processesList" class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.variables"
class="menu-item"
title="Coming..."
<a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.usersManagement"
class="menu-item"
title="Coming..."
<i class="mdi mdi-24px mdi-tools" />
</a>
<ul v-if="hasTools" class="menu text-left text-uppercase">
<li class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.variables"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.usersManagement"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span>
</a>
</li>
</ul>
</li>
</template>
<template #footer>
<li class="tab-item">
<a
class="tab-add"
:title="$t('message.openNewTab')"
@click="addQueryTab"
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span>
</a>
</li>
</ul>
</li>
<li slot="footer" class="tab-item">
<a
class="tab-add"
:title="$t('message.openNewTab')"
@click="addQueryTab"
>
<i class="mdi mdi-24px mdi-plus" />
</a>
</li>
<i class="mdi mdi-24px mdi-plus" />
</a>
</li>
</template>
</Draggable>
<WorkspaceEmptyState v-if="!workspace.tabs.length" @new-tab="addQueryTab" />
<template v-for="tab of workspace.tabs">
<WorkspaceQueryTab
<WorkspaceTabQuery
v-if="tab.type==='query'"
:key="tab.uid"
:tab="tab"
:is-selected="selectedTab === tab.uid"
:connection="connection"
/>
<WorkspaceTableTab
<WorkspaceTabTable
v-else-if="['temp-data', 'data'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
@@ -202,7 +327,15 @@
:schema="tab.schema"
:element-type="tab.elementType"
/>
<WorkspacePropsTab
<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"
@@ -210,7 +343,15 @@
:table="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabView
<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"
@@ -218,7 +359,16 @@
:view="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabTrigger
<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"
@@ -226,7 +376,16 @@
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabTriggerFunction
<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"
@@ -234,7 +393,16 @@
:function="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabRoutine
<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"
@@ -242,7 +410,16 @@
:routine="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabFunction
<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"
@@ -250,7 +427,16 @@
:function="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabScheduler
<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"
@@ -282,15 +468,24 @@ import Connection from '@/ipc-api/Connection';
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel';
import WorkspaceQueryTab from '@/components/WorkspaceQueryTab';
import WorkspaceTableTab from '@/components/WorkspaceTableTab';
import WorkspacePropsTab from '@/components/WorkspacePropsTab';
import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabTriggerFunction from '@/components/WorkspacePropsTabTriggerFunction';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
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';
@@ -301,15 +496,22 @@ export default {
WorkspaceEmptyState,
WorkspaceExploreBar,
WorkspaceEditConnectionPanel,
WorkspaceQueryTab,
WorkspaceTableTab,
WorkspacePropsTab,
WorkspacePropsTabView,
WorkspacePropsTabTrigger,
WorkspacePropsTabTriggerFunction,
WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabScheduler,
WorkspaceTabQuery,
WorkspaceTabTable,
WorkspaceTabNewTable,
WorkspaceTabPropsTable,
WorkspaceTabNewView,
WorkspaceTabPropsView,
WorkspaceTabNewTrigger,
WorkspaceTabPropsTrigger,
WorkspaceTabNewTriggerFunction,
WorkspaceTabPropsTriggerFunction,
WorkspaceTabNewRoutine,
WorkspaceTabNewFunction,
WorkspaceTabPropsRoutine,
WorkspaceTabPropsFunction,
WorkspaceTabNewScheduler,
WorkspaceTabPropsScheduler,
ModalProcessesList,
ModalDiscardChanges
},
@@ -365,7 +567,7 @@ export default {
return this.workspace ? this.workspace.selectedTab : null;
},
queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query');
return this.workspace ? this.workspace.tabs.filter(tab => tab.type === 'query') : [];
},
schemaChild () {
for (const key in this.workspace.breadcrumbs) {
@@ -373,28 +575,33 @@ export default {
if (this.workspace.breadcrumbs[key]) return this.workspace.breadcrumbs[key];
}
return false;
},
hasTools () {
return this.workspace.customizations.processesList ||
this.workspace.customizations.usersManagement ||
this.workspace.customizations.variables;
}
},
watch: {
selectedTab (newVal, oldVal) {
if (newVal !== oldVal) {
queryTabs: function (newVal, oldVal) {
if (newVal.length > oldVal.length) {
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);
const scroller = this.$refs.tabWrap;
if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth;
}, 0);
}
}
},
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);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addWorkspace: 'workspaces/addWorkspace',
@@ -408,6 +615,25 @@ export default {
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.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);
}
},
openAsPermanentTab (tab) {
const permanentTabs = {
table: 'data',
@@ -589,7 +815,7 @@ export default {
.th {
position: sticky;
top: 0;
border: 1px solid;
border: 2px solid;
border-left: none;
border-bottom-width: 2px;
padding: 0;
@@ -598,15 +824,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

@@ -11,6 +11,7 @@
<a class="tab-link">{{ $t('word.general') }}</a>
</li>
<li
v-if="customizations.sslConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
@@ -18,6 +19,7 @@
<a class="tab-link">{{ $t('word.ssl') }}</a>
</li>
<li
v-if="customizations.sshConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
@@ -49,25 +51,17 @@
</div>
<div class="column col-8 col-sm-12">
<select v-model="connection.client" class="form-select">
<option value="mysql">
MySQL
<option
v-for="client in clients"
:key="client.slug"
:value="client.slug"
>
{{ client.name }}
</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 v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
@@ -79,7 +73,20 @@
>
</div>
</div>
<div class="form-group columns">
<div v-if="customizations.fileConnection" 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">
<BaseUploadInput
:value="connection.databasePath"
:message="$t('word.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
@@ -105,7 +112,7 @@
>
</div>
</div>
<div class="form-group columns">
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
@@ -118,7 +125,7 @@
>
</div>
</div>
<div class="form-group columns">
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
@@ -144,7 +151,15 @@
>
</div>
</div>
<div class="form-group columns">
<div v-if="customizations.readOnlyMode" 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.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!customizations.fileConnection" 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">
@@ -309,6 +324,18 @@
/>
</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>
@@ -357,15 +384,23 @@ export default {
},
data () {
return {
clients: [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
],
connection: {
name: '',
client: 'mysql',
host: '127.0.0.1',
database: null,
databasePath: '',
port: null,
user: null,
password: '',
ask: false,
readonly: false,
uid: uidGen('C'),
ssl: false,
cert: '',
@@ -393,6 +428,13 @@ export default {
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();
@@ -432,7 +474,7 @@ export default {
try {
const res = await Connection.makeTest(this.connection);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
@@ -455,7 +497,7 @@ export default {
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}

View File

@@ -11,6 +11,7 @@
<a class="tab-link">{{ $t('word.general') }}</a>
</li>
<li
v-if="customizations.sslConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
@@ -18,6 +19,7 @@
<a class="tab-link">{{ $t('word.ssl') }}</a>
</li>
<li
v-if="customizations.sshConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
@@ -49,19 +51,17 @@
</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
v-for="client in clients"
:key="client.slug"
:value="client.slug"
>
{{ client.name }}
</option>
</select>
</div>
</div>
<div class="form-group columns">
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
@@ -73,7 +73,20 @@
>
</div>
</div>
<div class="form-group columns">
<div v-if="customizations.fileConnection" 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">
<BaseUploadInput
:value="localConnection.databasePath"
:message="$t('word.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
@@ -99,7 +112,7 @@
>
</div>
</div>
<div class="form-group columns">
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
@@ -112,7 +125,7 @@
>
</div>
</div>
<div class="form-group columns">
<div v-if="!customizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
@@ -138,7 +151,15 @@
>
</div>
</div>
<div class="form-group columns">
<div v-if="customizations.readOnlyMode" 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.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!customizations.fileConnection" 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">
@@ -303,6 +324,18 @@
/>
</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>
@@ -362,6 +395,12 @@ export default {
},
data () {
return {
clients: [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
],
isConnecting: false,
isTesting: false,
isAsking: false,
@@ -371,7 +410,7 @@ export default {
},
computed: {
customizations () {
return customizations[this.connection.client];
return customizations[this.localConnection.client];
},
isBusy () {
return this.isConnecting || this.isTesting;
@@ -414,7 +453,7 @@ export default {
try {
const res = await Connection.makeTest(this.localConnection);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
@@ -437,7 +476,7 @@ export default {
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
@@ -454,6 +493,7 @@ export default {
closeAsking () {
this.isTesting = false;
this.isAsking = false;
this.isConnecting = false;
},
selectTab (tab) {
this.selectedTab = tab;

View File

@@ -1,7 +1,16 @@
<template>
<div class="column col-12 empty">
<div class="empty-icon">
<img :src="require(`@/images/logo-${applicationTheme}.svg`).default" width="200">
<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') }}

View File

@@ -5,11 +5,15 @@
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.connectionStatus === 'connected'" class="workspace-explorebar-tools">
<i
v-if="customizations.schemas"
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewSchema')"
@click="showNewDBModal"
@@ -30,6 +34,7 @@
<div class="workspace-explorebar-search">
<div v-if="workspace.connectionStatus === 'connected'" class="has-icon-right">
<input
ref="searchInput"
v-model="searchTerm"
class="form-input input-sm"
type="text"
@@ -43,7 +48,7 @@
/>
</div>
</div>
<div class="workspace-explorebar-body">
<div class="workspace-explorebar-body" @click="$refs.explorebar.focus()">
<WorkspaceExploreBarSchema
v-for="db of workspace.structure"
:key="db.name"
@@ -61,67 +66,27 @@
@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"
/>
<ModalNewTriggerFunction
v-if="isNewTriggerFunctionModal"
:workspace="workspace"
@close="hideCreateTriggerFunctionModal"
@open-create-function-editor="openCreateTriggerFunctionEditor"
/>
<ModalNewScheduler
v-if="isNewSchedulerModal"
:workspace="workspace"
@close="hideCreateSchedulerModal"
@open-create-scheduler-editor="openCreateSchedulerEditor"
/>
<DatabaseContext
v-if="isDatabaseContext"
: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-trigger-function-modal="showCreateTriggerFunctionModal"
@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-show="isTableContext"
v-if="isTableContext"
:selected-schema="selectedSchema"
:selected-table="selectedTable"
:context-event="tableContextEvent"
@delete-table="deleteTable"
@duplicate-table="duplicateTable"
@close-context="closeTableContext"
@reload="refresh"
/>
@@ -138,11 +103,11 @@
:selected-misc="selectedMisc"
:selected-schema="selectedSchema"
:context-event="miscContextEvent"
@show-create-trigger-modal="showCreateTriggerModal"
@show-create-routine-modal="showCreateRoutineModal"
@show-create-function-modal="showCreateFunctionModal"
@show-create-trigger-function-modal="showCreateTriggerFunctionModal"
@show-create-scheduler-modal="showCreateSchedulerModal"
@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"
/>
@@ -154,8 +119,6 @@ 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';
@@ -165,13 +128,6 @@ import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext';
import ModalNewSchema from '@/components/ModalNewSchema';
import ModalNewTable from '@/components/ModalNewTable';
import ModalNewView from '@/components/ModalNewView';
import ModalNewTrigger from '@/components/ModalNewTrigger';
import ModalNewRoutine from '@/components/ModalNewRoutine';
import ModalNewFunction from '@/components/ModalNewFunction';
import ModalNewTriggerFunction from '@/components/ModalNewTriggerFunction';
import ModalNewScheduler from '@/components/ModalNewScheduler';
export default {
name: 'WorkspaceExploreBar',
@@ -181,14 +137,7 @@ export default {
TableContext,
MiscContext,
MiscFolderContext,
ModalNewSchema,
ModalNewTable,
ModalNewView,
ModalNewTrigger,
ModalNewRoutine,
ModalNewFunction,
ModalNewTriggerFunction,
ModalNewScheduler
ModalNewSchema
},
props: {
connection: Object,
@@ -199,7 +148,6 @@ export default {
isRefreshing: false,
isNewDBModal: false,
isNewTableModal: false,
isNewViewModal: false,
isNewTriggerModal: false,
isNewRoutineModal: false,
@@ -280,9 +228,12 @@ export default {
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) {
@@ -291,6 +242,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;
@@ -307,34 +271,17 @@ 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,
this.closeMiscFolderContext();
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Tables.createTable(params);
if (status === 'success') {
await this.refresh();
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'table',
type: 'table-props'
});
}
else
this.addNotification({ status: 'error', message: response });
elementName: '',
elementType: element,
type: `new-${element}`
});
},
openSchemaContext (payload) {
this.selectedSchema = payload.schema;
@@ -371,38 +318,6 @@ export default {
closeMiscFolderContext () {
this.isMiscFolderContext = false;
},
showCreateViewModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewViewModal = true;
},
hideCreateViewModal () {
this.isNewViewModal = false;
},
async openCreateViewEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Views.createView(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, view: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'view',
type: 'view-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateTriggerModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
@@ -411,31 +326,6 @@ export default {
hideCreateTriggerModal () {
this.isNewTriggerModal = false;
},
async openCreateTriggerEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Triggers.createTrigger(params);
if (status === 'success') {
await this.refresh();
const triggerName = this.customizations.triggerTableInName ? `${payload.table}.${payload.name}` : payload.name;
this.changeBreadcrumbs({ schema: this.selectedSchema, trigger: triggerName });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: triggerName,
elementType: 'trigger',
type: 'trigger-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateRoutineModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
@@ -444,30 +334,6 @@ export default {
hideCreateRoutineModal () {
this.isNewRoutineModal = false;
},
async openCreateRoutineEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Routines.createRoutine(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, routine: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'routine',
type: 'routine-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateFunctionModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
@@ -492,6 +358,89 @@ export default {
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,
@@ -596,6 +545,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>
@@ -18,17 +42,17 @@
@confirm="deleteMisc"
@hide="hideDeleteModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" />
<span class="cut-text">{{ deleteMessage }}</span>
</div>
</template>
<div slot="body">
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedMisc.name }}</b>"?
</div>
</div>
</template>
</ConfirmModal>
<ModalAskParameters
v-if="isAskingParameters"
@@ -78,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':
@@ -98,12 +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;
},
@@ -276,6 +302,68 @@ export default {
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

@@ -6,35 +6,35 @@
<div
v-if="selectedMisc === 'trigger'"
class="context-element"
@click="$emit('show-create-trigger-modal')"
@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('show-create-routine-modal')"
@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('show-create-function-modal')"
@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('show-create-trigger-function-modal')"
@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('show-create-scheduler-modal')"
@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>
@@ -74,9 +74,6 @@ export default {
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},

View File

@@ -9,7 +9,16 @@
<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">
@@ -34,7 +43,7 @@
<span v-html="highlightWord(table.name)" />
</a>
<div
v-if="table.type === 'table'"
v-if="table.type === 'table' && table.size !== false"
class="table-size tooltip tooltip-left mr-1"
:data-tooltip="formatBytes(table.size)"
>
@@ -63,14 +72,22 @@
:ref="breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@mousedown="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})"
@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>
@@ -97,7 +114,7 @@
:ref="breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name}"
@mousedown="selectMisc({schema: database.name, misc: procedure, type: 'routine'})"
@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'})"
>
@@ -131,7 +148,7 @@
: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="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})"
@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'})"
>
@@ -165,7 +182,7 @@
:ref="breadcrumbs.schema === database.name && breadcrumbs.function === func.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@mousedown="selectMisc({schema: database.name, misc: func, type: 'function'})"
@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'})"
>
@@ -199,14 +216,22 @@
:ref="breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@mousedown="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})"
@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>
@@ -320,11 +345,16 @@ export default {
this.addLoadedSchema(schema);
this.isLoading = false;
}
this.changeBreadcrumbs({ schema, table: null });
},
selectTable ({ schema, table }) {
this.newTab({ uid: this.connection.uid, elementName: table.name, schema: this.database.name, type: 'temp-data', elementType: table.type });
this.newTab({
uid: this.connection.uid,
elementName: table.name,
schema: this.database.name,
type: 'temp-data',
elementType: table.type
});
this.setBreadcrumbs({ schema, [table.type]: table.name });
},
selectMisc ({ schema, misc, type }) {
@@ -421,6 +451,11 @@ export default {
position: sticky;
top: 0;
z-index: 2;
.schema-size {
visibility: hidden;
width: 22.5px;
}
}
.database-name,
@@ -466,6 +501,10 @@ export default {
.misc-name {
&:hover {
border-radius: $border-radius;
.schema-size {
visibility: visible;
}
}
}
@@ -495,7 +534,9 @@ export default {
}
}
.table-size {
.schema-size,
.table-size,
.disabled-indicator {
position: absolute;
right: 0;
top: 0;

View File

@@ -10,49 +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="showCreateTriggerFunctionModal"
@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>
@@ -65,7 +65,11 @@
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div>
<div class="context-element" @click="showDeleteModal">
<div
v-if="workspace.customizations.schemaDrop"
class="context-element"
@click="showDeleteModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div>
@@ -74,17 +78,17 @@
@confirm="deleteSchema"
@hide="hideDeleteModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" />
<span class="cut-text">{{ $t('message.deleteSchema') }}</span>
</div>
</template>
<div slot="body">
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedSchema }}</b>"?
</div>
</div>
</template>
</ConfirmModal>
<ModalEditSchema
v-if="isEditModal"
@@ -132,26 +136,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');
},
showCreateTriggerFunctionModal () {
this.$emit('show-create-trigger-function-modal');
openCreateTriggerFunctionTab () {
this.$emit('open-create-trigger-function-tab');
},
showCreateSchedulerModal () {
this.$emit('show-create-scheduler-modal');
openCreateSchedulerTab () {
this.$emit('open-create-scheduler-tab');
},
showDeleteModal () {
this.isDeleteModal = true;

View File

@@ -40,33 +40,33 @@
@confirm="emptyTable"
@hide="hideEmptyModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-off mr-1" /> <span class="cut-text">{{ $t('message.emptyTable') }}</span>
</div>
</template>
<div slot="body">
<template #body>
<div class="mb-2">
{{ $t('message.emptyCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
</div>
</div>
</template>
</ConfirmModal>
<ConfirmModal
v-if="isDeleteModal"
@confirm="deleteTable"
@hide="hideDeleteModal"
>
<template slot="header">
<template #header>
<div class="d-flex">
<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">
<template #body>
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
</div>
</div>
</template>
</ConfirmModal>
</BaseContextMenu>
</template>
@@ -76,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',
@@ -116,9 +115,6 @@ export default {
removeLoadingElement: 'workspaces/removeLoadingElement',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},
@@ -166,36 +162,8 @@ export default {
this.closeContext();
},
async duplicateTable () {
this.closeContext();
this.addLoadingElement({
name: this.selectedTable.name,
schema: this.selectedSchema,
type: 'table'
});
try {
const { status, response } = await Tables.duplicateTable({
uid: this.selectedWorkspace,
table: this.selectedTable.name,
schema: this.selectedSchema
});
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'
});
duplicateTable () {
this.$emit('duplicate-table', { schema: this.selectedSchema, table: this.selectedTable });
},
async emptyTable () {
this.closeContext();
@@ -228,57 +196,8 @@ export default {
type: 'table'
});
},
async deleteTable () {
this.closeContext();
this.addLoadingElement({
name: this.selectedTable.name,
schema: this.selectedSchema,
type: 'table'
});
try {
let res;
if (this.selectedTable.type === 'table') {
res = await Tables.dropTable({
uid: this.selectedWorkspace,
table: this.selectedTable.name,
schema: this.selectedSchema
});
}
else if (this.selectedTable.type === 'view') {
res = await Views.dropView({
uid: this.selectedWorkspace,
view: this.selectedTable.name,
schema: this.selectedSchema
});
}
const { status, response } = res;
if (status === 'success') {
this.removeTabs({
uid: this.selectedWorkspace,
elementName: this.selectedTable.name,
elementType: this.selectedTable.type,
schema: this.selectedSchema
});
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'
});
deleteTable () {
this.$emit('delete-table', { schema: this.selectedSchema, table: this.selectedTable });
}
}
};

View File

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

View File

@@ -1,41 +0,0 @@
<template>
<div class="container">
<div class="columns">
<div class="column col-16 text-right">
<div class="mb-4">
{{ $t('message.runQuery') }}
</div>
<div class="mb-4">
{{ $t('word.format') }}
</div>
<div class="mb-4">
{{ $t('word.clear') }}
</div>
</div>
<div class="column col-16">
<div class="mb-4">
<code>F5</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>F8</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>W</code>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'WorkspaceQueryEmptyState'
};
</script>
<style scoped>
.container {
padding-top: 15vh;
opacity: 0.6;
}
</style>

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,455 @@
<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 || !isValid"
: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);
},
isValid () {
return !!this.localFields.length && !!this.localOptions.name.trim().length;
}
},
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 || !this.isValid) 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>

View File

@@ -0,0 +1,279 @@
<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>
</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.triggerFunctionlanguages" 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.triggerFunctionlanguages" :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 v-if="customizations.comment" class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
</label>
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</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>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspaceTabNewTriggerFunction',
components: {
BaseLoader,
QueryEditor
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
isAskingParameters: 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.triggerFunctionSql,
language: this.customizations.triggerFunctionlanguages.length ? this.customizations.triggerFunctionlanguages[0] : null,
name: ''
};
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);
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.localFunction
};
try {
const { status, response } = await Functions.createTriggerFunction(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localFunction.name,
elementType: 'triggerFunction',
type: 'trigger-function-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, triggerFunction: 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();
}
},
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,287 @@
<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="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localView.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalView.definer">
{{ originalView.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 col-auto mr-2">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<select v-model="localView.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<select v-model="localView.algorithm" class="form-select">
<option>UNDEFINED</option>
<option>MERGE</option>
<option>TEMPTABLE</option>
</select>
</div>
</div>
<div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<select v-model="localView.updateOption" class="form-select">
<option value="">
None
</option>
<option>CASCADED</option>
<option>LOCAL</option>
</select>
</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.selectStatement') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import Views from '@/ipc-api/Views';
export default {
name: 'WorkspaceTabNewView',
components: {
BaseLoader,
QueryEditor
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
originalView: {},
localView: {},
lastView: 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.originalView) !== JSON.stringify(this.localView);
},
isDefinerInUsers () {
return this.originalView ? this.workspace.users.some(user => this.originalView.definer === `\`${user.name}\`@\`${user.host}\``) : true;
}
},
watch: {
isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, view: this.view });
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
}
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
this.originalView = {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
};
this.localView = JSON.parse(JSON.stringify(this.originalView));
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',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
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.localView
};
try {
const { status, response } = await Views.createView(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localView.name,
elementType: 'view',
type: 'view-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, view: this.localView.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localView = JSON.parse(JSON.stringify(this.originalView));
this.$refs.queryEditor.editor.session.setValue(this.localView.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();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

View File

@@ -37,10 +37,6 @@
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
@@ -49,6 +45,152 @@
</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"
:class="{'is-error': !isTableNameValid}"
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>
<option v-if="!isInDataTypes">
{{ localFunction.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="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>
@@ -61,14 +203,7 @@
:height="editorHeight"
/>
</div>
<WorkspacePropsFunctionOptionsModal
v-if="isOptionsModal"
:local-options="localFunction"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsFunctionParamsModal
<WorkspaceTabPropsFunctionParamsModal
v-if="isParamsModal"
:local-parameters="localFunction.parameters"
:workspace="workspace"
@@ -91,18 +226,16 @@ import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsFunctionOptionsModal from '@/components/WorkspacePropsFunctionOptionsModal';
import WorkspacePropsFunctionParamsModal from '@/components/WorkspacePropsFunctionParamsModal';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspacePropsTabFunction',
name: 'WorkspaceTabPropsFunction',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsFunctionOptionsModal,
WorkspacePropsFunctionParamsModal,
WorkspaceTabPropsFunctionParamsModal,
ModalAskParameters
},
props: {
@@ -115,7 +248,6 @@ export default {
return {
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
@@ -133,6 +265,9 @@ export default {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
@@ -144,6 +279,19 @@ export default {
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
isTableNameValid () {
return this.localFunction.name !== '';
},
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.localFunction.returns);
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
@@ -225,7 +373,7 @@ export default {
this.originalFunction = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => {
param._id = uidGen();
param._antares_id = uidGen();
return param;
})];
@@ -330,12 +478,6 @@ export default {
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},

View File

@@ -6,13 +6,13 @@
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ func }}"</span>
</div>
</template>
<div :slot="'body'">
<template #body>
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
@@ -36,10 +36,10 @@
<div ref="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param._id"
:key="param._antares_id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-element': selectedParam === param._id}"
@click="selectParameter($event, param._id)"
:class="{'selected-element': selectedParam === param._antares_id}"
@click="selectParameter($event, param._antares_id)"
>
<div class="tile-icon">
<div>
@@ -56,7 +56,7 @@
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeParameter(param._id)"
@click.prevent="removeParameter(param._antares_id)"
>
<i class="mdi mdi-close" />
</button>
@@ -167,7 +167,7 @@
</div>
</div>
</div>
</div>
</template>
</ConfirmModal>
</template>
@@ -176,7 +176,7 @@ import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineParamsModal',
name: 'WorkspaceTabPropsFunctionParamsModal',
components: {
ConfirmModal
},
@@ -196,7 +196,7 @@ export default {
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param._id === this.selectedParam);
return this.parametersProxy.find(param => param._antares_id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
@@ -237,12 +237,13 @@ export default {
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addParameter () {
const newUid = uidGen();
this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`,
type: 'INT',
_antares_id: newUid,
name: `param${this.i++}`,
type: this.workspace.dataTypes[0].types[0].name,
context: 'IN',
length: 10
length: ''
}];
if (this.parametersProxy.length === 1)
@@ -250,12 +251,13 @@ export default {
setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
this.selectedParam = newUid;
}, 20);
},
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
this.parametersProxy = this.parametersProxy.filter(param => param._antares_id !== uid);
if (this.selectedParam === name && this.parametersProxy.length)
if (this.parametersProxy.length && this.selectedParam === uid)
this.resetSelectedID();
},
clearChanges () {
@@ -266,7 +268,7 @@ export default {
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._antares_id : '';
}
}
};

View File

@@ -37,10 +37,6 @@
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
@@ -49,6 +45,108 @@
</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"
:class="{'is-error': !isTableNameValid}"
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>
@@ -62,14 +160,7 @@
:height="editorHeight"
/>
</div>
<WorkspacePropsRoutineOptionsModal
v-if="isOptionsModal"
:local-options="localRoutine"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsRoutineParamsModal
<WorkspaceTabPropsRoutineParamsModal
v-if="isParamsModal"
:local-parameters="localRoutine.parameters"
:workspace="workspace"
@@ -92,18 +183,16 @@ import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Routines from '@/ipc-api/Routines';
export default {
name: 'WorkspacePropsTabRoutine',
name: 'WorkspaceTabPropsRoutine',
components: {
QueryEditor,
BaseLoader,
WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal,
WorkspaceTabPropsRoutineParamsModal,
ModalAskParameters
},
props: {
@@ -116,7 +205,6 @@ export default {
return {
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalRoutine: null,
@@ -134,6 +222,9 @@ export default {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
@@ -143,6 +234,9 @@ export default {
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
isTableNameValid () {
return this.localRoutine.name !== '';
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
@@ -224,7 +318,7 @@ export default {
this.originalRoutine = response;
this.originalRoutine.parameters = [...this.originalRoutine.parameters.map(param => {
param._id = uidGen();
param._antares_id = uidGen();
return param;
})];
@@ -327,12 +421,6 @@ export default {
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},

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