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

Compare commits

..

100 Commits

Author SHA1 Message Date
4519829aa2 chore(release): 0.5.8 2022-07-02 15:32:02 +02:00
a45d76e8b4 fix: exception on new scheduler tab 2022-06-30 16:42:29 +02:00
7702ca025f fix: error on modals missing focusable elements 2022-06-30 10:05:35 +02:00
e97da37103 feat: context shortcut to disconnect from left bar 2022-06-29 13:17:33 +02:00
6573fe69ac fix: connection string field doesn't appear switching to postgre when editing a connection 2022-06-29 11:20:08 +02:00
902c29ffa5 feat(MySQL): option to disable foreign key checks when empty a table 2022-06-29 10:48:21 +02:00
5f57a9f60d fix: ctrl+a on results doesn't work properly 2022-06-28 17:55:18 +02:00
822af44a47 refactor: minor improvements 2022-06-27 18:28:04 +02:00
e42c424a13 fix: focus goes outside modals with tab key navigation 2022-06-26 15:07:37 +02:00
0a3a4827dd fix: result table cells/rows not loses focus clicking outside 2022-06-26 10:21:17 +02:00
a80d227400 fix(Windows): white window buttons with dark theme 2022-06-24 18:17:37 +02:00
cfd82c8f41 fix: editor gutter pin not working 2022-06-24 17:26:28 +02:00
91d0735a5f fix: double context menu on table settings rows 2022-06-23 23:11:43 +02:00
93ce619782 fix(Windows): Windows 7 style window frame at startup 2022-06-23 11:57:25 +02:00
8f01740475 fix(UI): wrong tables scrollable height after switching tabs 2022-06-22 18:47:32 +02:00
869c75f654 Merge pull request #324 from antares-sql/dependabot/npm_and_yarn/electron-19.0.5
build(deps-dev): bump electron from 17.4.3 to 19.0.5
2022-06-21 21:44:06 +02:00
dependabot[bot]
14aff67d2d build(deps-dev): bump electron from 17.4.3 to 19.0.5
Bumps [electron](https://github.com/electron/electron) from 17.4.3 to 19.0.5.
- [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/v17.4.3...v19.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-21 16:11:11 +00:00
174579bf8c Merge pull request #248 from antares-sql/ts-renderer
TypeScript in renderer process
2022-06-21 18:09:42 +02:00
0eab0a7140 Merge branch 'master' of https://github.com/antares-sql/antares into ts-renderer 2022-06-21 18:08:43 +02:00
a103617ce8 refactor: ts and composition api on missing components 2022-06-21 17:54:47 +02:00
61ec3cbd4e Merge branch 'master' of https://github.com/antares-sql/antares 2022-06-20 08:49:29 +02:00
1765d9fd0b chore: dependabot monthly interval 2022-06-20 08:49:26 +02:00
329656ff1d Merge pull request #309 from toriphes/feat/base-select-max-visible-options
feat: add max visible options prop
2022-06-20 08:46:47 +02:00
Giulio Ganci
067a6f3507 feat: add max visible options prop 2022-06-19 16:58:52 +02:00
d12c6f5210 chore(release): 0.5.7 2022-06-19 16:11:12 +02:00
89e8d9fcdb refactor: ts and composition api on WorkspaceTabNew* components 2022-06-14 20:02:17 +02:00
ee623b0a0f fix: unable to add new table fields 2022-06-14 11:48:36 +02:00
33a4663694 Merge branch 'master' of https://github.com/antares-sql/antares into ts-renderer 2022-06-13 09:29:05 +02:00
c37138c6f5 ci: fix linux upload artifacts github action [skip ci] 2022-06-12 19:58:22 +02:00
e754877ee6 Merge branch 'master' of https://github.com/antares-sql/antares 2022-06-12 18:30:48 +02:00
ed38a9e7ff ci: linux upload artifacts github action 2022-06-12 18:30:44 +02:00
6bad032f0d fix(Linux): setting bar tooltip position 2022-06-11 14:26:51 +02:00
d214c1f35b fix: reload tab content on tab sort 2022-06-11 01:11:08 +02:00
77d9cac092 fix: fields sorting in table setting tabs 2022-06-10 23:29:34 +02:00
cba2ce2e37 fix: selected foreign key value not visible in the insert row modal 2022-06-10 20:02:26 +02:00
bd46d17424 refactor: ts and composition api on WorkspaceExplorebar* components 2022-06-09 20:08:32 +02:00
5b33419b64 fix: exception on app start setting window title 2022-06-08 14:55:29 +02:00
00242697a1 feat: dynamic app window title 2022-06-08 13:24:14 +02:00
85cec05f70 perf(Linux): title bar improvements 2022-06-08 13:04:19 +02:00
5fa8bf38e4 perf(Windows): title bar improvements 2022-06-07 18:32:37 +02:00
23acf00def fix: main process not closed after window close on some conditions 2022-06-07 09:33:03 +02:00
be70b5be7f refactor: ts on ipc api 2022-06-05 17:57:44 +02:00
7fc01227e7 refactor: ts and composition api on more components 2022-06-04 18:37:16 +02:00
1c666a07d8 Merge pull request #289 from toriphes/feat/hotkeys
Feat: Hokeys to navigate between tabs and result sets
2022-06-04 15:20:08 +02:00
Giulio Ganci
49abd1ea7f feat: hotkeys to navigate inside a table resultset 2022-06-04 08:45:48 +02:00
Giulio Ganci
d3b9e08446 feat: hotkeys to navigate forward or backward between tabs 2022-06-03 18:56:19 +02:00
20b814378b chore: package.json changes 2022-06-02 12:50:05 +02:00
8ce1d1a964 chore(release): 0.5.6 2022-06-02 11:31:10 +02:00
d151c7254e build: Windows icon improvements 2022-06-02 11:17:53 +02:00
26aad519df perf: improved precision of MariaDB or MySQL auto detection 2022-06-02 09:50:16 +02:00
31b7999bba fix: empty query tab schema select if no schema selected 2022-06-02 09:46:49 +02:00
caf776bd55 fix: inline field update not working with tables missing primary key 2022-06-01 18:31:25 +02:00
a7d5e1973c fix(SQLite): unable to insert rows with TEXT fields 2022-06-01 09:56:45 +02:00
8870304c15 fix(UI): select closes clicking on scrollbar 2022-05-29 16:42:41 +02:00
2007305ff0 refactor: ts on pinia store 2022-05-28 18:43:56 +02:00
e97401e27d Merge branch 'master' of https://github.com/antares-sql/antares into ts-renderer 2022-05-25 14:41:15 +02:00
62f6fd16d5 refactor: ts on i18n 2022-05-24 23:02:40 +02:00
cdca6eaa35 refactor: ts and composition api for modals 2022-05-24 23:02:14 +02:00
34e8d3e5b1 chore(release): 0.5.5 2022-05-24 14:25:04 +02:00
6c8a36e947 refactor(UI): double click to edit field type or collation 2022-05-23 16:35:50 +02:00
99b1c1be12 Merge pull request #245 from toriphes/feat/advanced-dropdown-list
feat/advanced-dropdown-list
2022-05-23 16:31:19 +02:00
3991382153 Merge branch 'feat/advanced-dropdown-list' of https://github.com/toriphes/antares into pr/toriphes/245 2022-05-23 16:19:35 +02:00
allcontributors[bot]
3b57b7ef3b docs: update .all-contributorsrc [skip ci] 2022-05-23 16:13:51 +02:00
allcontributors[bot]
45d599ad7f docs: update README.md [skip ci] 2022-05-23 16:13:51 +02:00
9082960310 feat(translation): russian translation, closes #266 2022-05-23 16:11:54 +02:00
Giulio Ganci
5398964190 feat: added dropdown animation 2022-05-22 15:45:16 +02:00
5d5f1da97b perf(UI): max height for query text area increased 2022-05-19 09:30:43 +02:00
84826ff4c0 refactor: ts and composition api on more elements 2022-05-17 19:11:31 +02:00
c95c593c74 chore: create CODE_OF_CONDUCT.md 2022-05-15 19:08:06 +02:00
5a50ba88e8 Merge branch 'master' of https://github.com/antares-sql/antares into ts-renderer 2022-05-15 18:23:09 +02:00
c5baf2b0d3 fix: query tab content disappears reordering or closing other tabs, closes #261 2022-05-15 18:15:05 +02:00
a082514f88 fix(PostgreSQL): idle timeout disabled 2022-05-15 17:37:54 +02:00
c826888b0d fix: SSH tunnel connection error with private key, closes #260 2022-05-14 11:24:24 +02:00
8a55b36527 refactor: ts and composition api for single instance components 2022-05-14 11:15:42 +02:00
b0d464952f Merge branch 'master' of https://github.com/antares-sql/antares into pr/toriphes/245 2022-05-14 09:57:09 +02:00
Giulio Ganci
7c45203636 fix(UI): BaseSelect keyboard navigation 2022-05-13 18:35:02 +02:00
Giulio Ganci
71b0736d0d fix(UI): BaseSelect style 2022-05-13 18:20:47 +02:00
Giulio Ganci
42bc9196ff feat(UI): select tab replace with BaseSelect component 2022-05-11 23:30:31 +02:00
Giulio Ganci
f7e04d6333 feat(UI): BaseSelect supports disabled options 2022-05-11 22:57:13 +02:00
45b2eb2934 fix: reactivity problem on BaseVirtualScroll component 2022-05-11 11:27:29 +02:00
9ee1b3023d build: electron-updater downgrade [skip ci] 2022-05-10 19:16:06 +02:00
79d9acb471 chore(release): 0.5.4 2022-05-10 18:13:30 +02:00
e02565c0d9 perf(UI): left alignment for numbers in result tables, closes #249 2022-05-10 15:19:47 +02:00
ff272440bd fix: unable to insert auto-generated datetime fields 2022-05-10 15:14:34 +02:00
e62f280528 fix: app blocked by BIT fields with no default, closes #256 2022-05-10 15:13:08 +02:00
6d6151814e fix: SSH tunnel not working 2022-05-10 14:44:45 +02:00
58611bf07f fix: file upload input not working 2022-05-10 14:43:08 +02:00
d494b17df7 fix: 2022-05-10 13:22:26 +02:00
ae377a6c3c refactor: ts and composition api for base components 2022-05-10 13:02:01 +02:00
cc5910b88f refactor: common to ts 2022-05-10 12:57:25 +02:00
Giulio Ganci
2b436d8613 feat(UI): BaseSelect disabled state 2022-05-10 10:29:38 +02:00
Giulio Ganci
1869e6a148 feat(UI): BaseSelect supports option groups 2022-05-09 17:31:58 +02:00
d1bfa282c3 build: ts config for renderer 2022-05-09 11:48:30 +02:00
Giulio Ganci
302c66457d feat(UI): ForeignKeySelect implements BaseSelect component 2022-05-09 11:29:25 +02:00
Giulio Ganci
0043d07708 feat(UI): BaseSelect option list scrolls automatically using up/down keys 2022-05-08 18:59:00 +02:00
Ngô Quốc Đạt
e0f85f469f Added missing translations for vi-VN 2022-05-08 16:29:48 +02:00
Giulio Ganci
a037d0cc01 feat(UI): BaseSelect in table filters 2022-05-08 13:15:39 +02:00
Giulio Ganci
5582a12bbf feat(UI): BaseSelect small variant 2022-05-08 13:14:40 +02:00
Giulio Ganci
22622df2cf feat(UI): initial BaseSelect integration 2022-05-08 09:45:37 +02:00
Giulio Ganci
745d551cc9 feat(UI): new BaseSelect component 2022-05-08 09:44:52 +02:00
192 changed files with 44320 additions and 12955 deletions

View File

@@ -165,6 +165,15 @@
"contributions": [ "contributions": [
"translation" "translation"
] ]
},
{
"login": "xak666",
"name": "xaka_xak",
"avatar_url": "https://avatars.githubusercontent.com/u/38811437?v=4",
"profile": "https://github.com/xak666",
"contributions": [
"translation"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -1,4 +1,5 @@
node_modules node_modules
assets assets
out out
dist dist
build

View File

@@ -8,4 +8,4 @@ updates:
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "monthly"

View File

@@ -1,4 +1,4 @@
name: Build/release [linux] name: Build/release [LINUX]
on: push on: push

View File

@@ -1,4 +1,4 @@
name: Build/release [mac] name: Build/release [MAC]
on: push on: push

View File

@@ -1,4 +1,4 @@
name: Build/release [windows] name: Build/release [WINDOWS]
on: push on: push

View File

@@ -0,0 +1,26 @@
name: Create artifact [LINUX]
on:
workflow_dispatch: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: npm install & build
run: |
npm install
npm run build:local
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: linux-build
retention-days: 3
path: |
build
!build/*-unpacked
!build/.icon-ico

View File

@@ -1,4 +1,4 @@
name: Test end-to-end [linux] name: Test end-to-end [LINUX]
on: push on: push

1
.gitignore vendored
View File

@@ -7,5 +7,4 @@ node_modules
thumbs.db thumbs.db
NOTES.md NOTES.md
*.txt *.txt
package-lock.json
*.heapsnapshot *.heapsnapshot

View File

@@ -5,7 +5,9 @@
"MySQL", "MySQL",
"PostgreSQL", "PostgreSQL",
"SQLite", "SQLite",
"Windows" "Windows",
"translation",
"Linux"
], ],
"svg.preview.background": "transparent" "svg.preview.background": "transparent"
} }

View File

@@ -2,6 +2,141 @@
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. 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.5.8](https://github.com/antares-sql/antares/compare/v0.5.7...v0.5.8) (2022-07-02)
### Features
* add max visible options prop ([067a6f3](https://github.com/antares-sql/antares/commit/067a6f350757c1e6b4df51f801ae832b47bd3484))
* context shortcut to disconnect from left bar ([e97da37](https://github.com/antares-sql/antares/commit/e97da3710385690b85391938e40145a1591bc2e8))
* **MySQL:** option to disable foreign key checks when empty a table ([902c29f](https://github.com/antares-sql/antares/commit/902c29ffa551bc3489fa1d9136ee926d135ea14f))
### Bug Fixes
* connection string field doesn't appear switching to postgre when editing a connection ([6573fe6](https://github.com/antares-sql/antares/commit/6573fe69aca2b99c7a700879fb0d0930e864cbe6))
* ctrl+a on results doesn't work properly ([5f57a9f](https://github.com/antares-sql/antares/commit/5f57a9f60d281e24e5bee4330c081fa5d8651b36))
* double context menu on table settings rows ([91d0735](https://github.com/antares-sql/antares/commit/91d0735a5f4861bc6ad13b9285ea7a9bd7be9538))
* editor gutter pin not working ([cfd82c8](https://github.com/antares-sql/antares/commit/cfd82c8f419952879b386187eb146847098263fe))
* error on modals missing focusable elements ([7702ca0](https://github.com/antares-sql/antares/commit/7702ca025fcae6209ae3851d0ccd25579f93e243))
* exception on new scheduler tab ([a45d76e](https://github.com/antares-sql/antares/commit/a45d76e8b4ecdecf53438fe174f61ea32f4e10ac))
* focus goes outside modals with tab key navigation ([e42c424](https://github.com/antares-sql/antares/commit/e42c424a13a6901414a1a1c4e2f68cb4ddef7d59))
* reactivity problem on BaseVirtualScroll component ([45b2eb2](https://github.com/antares-sql/antares/commit/45b2eb2934b9f7a08f379ad4d7a44b1c89585449))
* result table cells/rows not loses focus clicking outside ([0a3a482](https://github.com/antares-sql/antares/commit/0a3a4827dd75539666fa2c827415af3bfa224543))
* **UI:** wrong tables scrollable height after switching tabs ([8f01740](https://github.com/antares-sql/antares/commit/8f01740475ea6d5d9b5eefabdbf27099df76f2cf))
* **Windows:** white window buttons with dark theme ([a80d227](https://github.com/antares-sql/antares/commit/a80d22740045a61fd14fd5da401c0d123d54f4de))
* **Windows:** Windows 7 style window frame at startup ([93ce619](https://github.com/antares-sql/antares/commit/93ce619782d58cfb8fb1ecce2ca2137a61ec6181))
### [0.5.7](https://github.com/antares-sql/antares/compare/v0.5.4...v0.5.7) (2022-06-19)
### Features
* added dropdown animation ([5398964](https://github.com/antares-sql/antares/commit/539896419064db9127f6a72acdbb11af2c4aa60a))
* dynamic app window title ([0024269](https://github.com/antares-sql/antares/commit/00242697a102f82dd0c731a3529c984fbdf83b3e))
* hotkeys to navigate forward or backward between tabs ([d3b9e08](https://github.com/antares-sql/antares/commit/d3b9e08446708654b3c6fad565b734d93effe683))
* hotkeys to navigate inside a table resultset ([49abd1e](https://github.com/antares-sql/antares/commit/49abd1ea7f5ec368e9a9201f8fd5b6520c4bd0a8))
* **translation:** russian translation, closes [#266](https://github.com/antares-sql/antares/issues/266) ([9082960](https://github.com/antares-sql/antares/commit/9082960310573a6e4d14bfbe82ed2eb1489f308d))
* **UI:** BaseSelect disabled state ([2b436d8](https://github.com/antares-sql/antares/commit/2b436d8613a1e3dff55d73adbddf5d2cd2452f27))
* **UI:** BaseSelect in table filters ([a037d0c](https://github.com/antares-sql/antares/commit/a037d0cc0148444e8e6c5b87c79f6ba9c2a6f0fe))
* **UI:** BaseSelect option list scrolls automatically using up/down keys ([0043d07](https://github.com/antares-sql/antares/commit/0043d077081fc49724722a5d5a74986d990c539d))
* **UI:** BaseSelect small variant ([5582a12](https://github.com/antares-sql/antares/commit/5582a12bbfade75dbcc7f9d71ada7190ed08d3c2))
* **UI:** BaseSelect supports disabled options ([f7e04d6](https://github.com/antares-sql/antares/commit/f7e04d633340a53420ce1c434e906c9434620e6e))
* **UI:** BaseSelect supports option groups ([1869e6a](https://github.com/antares-sql/antares/commit/1869e6a1482daf9381d9ac2244bf0aeffa758edc))
* **UI:** ForeignKeySelect implements BaseSelect component ([302c664](https://github.com/antares-sql/antares/commit/302c66457deeb50facf4735291640fcf48b78f66))
* **UI:** initial BaseSelect integration ([22622df](https://github.com/antares-sql/antares/commit/22622df2cfcb71054c6f6110b7ad9d4f635553dc))
* **UI:** new BaseSelect component ([745d551](https://github.com/antares-sql/antares/commit/745d551cc9253eae4e39e5d3406ceee051a7d6c1))
* **UI:** select tab replace with BaseSelect component ([42bc919](https://github.com/antares-sql/antares/commit/42bc9196ffc2f64b77f9cb42136255fc74815034))
### Bug Fixes
* empty query tab schema select if no schema selected ([31b7999](https://github.com/antares-sql/antares/commit/31b7999bba5d115913d42087614b9888bc761068))
* exception on app start setting window title ([5b33419](https://github.com/antares-sql/antares/commit/5b33419b6421d7d198a978e79e22d0a76306cdb4))
* fields sorting in table setting tabs ([77d9cac](https://github.com/antares-sql/antares/commit/77d9cac092fbb806810c3463ca066395fcab5307))
* inline field update not working with tables missing primary key ([caf776b](https://github.com/antares-sql/antares/commit/caf776bd55606c793c9763c204aa9f05d1feb27f))
* **Linux:** setting bar tooltip position ([6bad032](https://github.com/antares-sql/antares/commit/6bad032f0d1094736f651b6c06a60d2a0df36c98))
* main process not closed after window close on some conditions ([23acf00](https://github.com/antares-sql/antares/commit/23acf00def77b5662e48b84591a31760737774a7))
* **PostgreSQL:** idle timeout disabled ([a082514](https://github.com/antares-sql/antares/commit/a082514f88040c7e0ffdf4e8357bab45370a4c39))
* query tab content disappears reordering or closing other tabs, closes [#261](https://github.com/antares-sql/antares/issues/261) ([c5baf2b](https://github.com/antares-sql/antares/commit/c5baf2b0d379fdd28ee8cb907628bbfca940e2f6))
* reload tab content on tab sort ([d214c1f](https://github.com/antares-sql/antares/commit/d214c1f35ba231a8a01dbe8c0faad07d4b337752))
* selected foreign key value not visible in the insert row modal ([cba2ce2](https://github.com/antares-sql/antares/commit/cba2ce2e37cedbf0b242cc474b37bf052009ae62))
* **SQLite:** unable to insert rows with TEXT fields ([a7d5e19](https://github.com/antares-sql/antares/commit/a7d5e1973cd59d7d0ef1e74bdcf44d87fba43559))
* SSH tunnel connection error with private key, closes [#260](https://github.com/antares-sql/antares/issues/260) ([c826888](https://github.com/antares-sql/antares/commit/c826888b0dd0908958a4f727ddfa642e846269cf))
* **UI:** BaseSelect keyboard navigation ([7c45203](https://github.com/antares-sql/antares/commit/7c452036368fa0db6b9cde7c35e60a8e57bfece7))
* **UI:** BaseSelect style ([71b0736](https://github.com/antares-sql/antares/commit/71b0736d0ddbd599ab41cde0a6b0823e2bb7da2f))
* **UI:** select closes clicking on scrollbar ([8870304](https://github.com/antares-sql/antares/commit/8870304c15346257a90193807b9ae07c1393e3e2))
* unable to add new table fields ([ee623b0](https://github.com/antares-sql/antares/commit/ee623b0a0f121df0ac53d49d8be437c76ddb8539))
### Improvements
* improved precision of MariaDB or MySQL auto detection ([26aad51](https://github.com/antares-sql/antares/commit/26aad519df6ea1bbc7dffbf540193a7b2ed9ae2a))
* **Linux:** title bar improvements ([85cec05](https://github.com/antares-sql/antares/commit/85cec05f7037a1339ee223554cf127693a527aa1))
* **UI:** max height for query text area increased ([5d5f1da](https://github.com/antares-sql/antares/commit/5d5f1da97b9adfa743197d8fa0bbb6addd565a7a))
* **Windows:** title bar improvements ([5fa8bf3](https://github.com/antares-sql/antares/commit/5fa8bf38e433ef2fb31bcb893cd9e75549bd6a49))
### [0.5.6](https://github.com/antares-sql/antares/compare/v0.5.4...v0.5.6) (2022-06-02)
### Bug Fixes
* empty query tab schema select if no schema selected ([31b7999](https://github.com/antares-sql/antares/commit/31b7999bba5d115913d42087614b9888bc761068))
* inline field update not working with tables missing primary key ([caf776b](https://github.com/antares-sql/antares/commit/caf776bd55606c793c9763c204aa9f05d1feb27f))
* **SQLite:** unable to insert rows with TEXT fields ([a7d5e19](https://github.com/antares-sql/antares/commit/a7d5e1973cd59d7d0ef1e74bdcf44d87fba43559))
* **UI:** select closes clicking on scrollbar ([8870304](https://github.com/antares-sql/antares/commit/8870304c15346257a90193807b9ae07c1393e3e2))
### Improvements
* improved precision of MariaDB or MySQL auto detection ([26aad51](https://github.com/antares-sql/antares/commit/26aad519df6ea1bbc7dffbf540193a7b2ed9ae2a))
### [0.5.5](https://github.com/antares-sql/antares/compare/v0.5.4...v0.5.5) (2022-05-24)
### Features
* added dropdown animation ([5398964](https://github.com/antares-sql/antares/commit/539896419064db9127f6a72acdbb11af2c4aa60a))
* **translation:** russian translation, closes [#266](https://github.com/antares-sql/antares/issues/266) ([9082960](https://github.com/antares-sql/antares/commit/9082960310573a6e4d14bfbe82ed2eb1489f308d))
* **UI:** BaseSelect disabled state ([2b436d8](https://github.com/antares-sql/antares/commit/2b436d8613a1e3dff55d73adbddf5d2cd2452f27))
* **UI:** BaseSelect in table filters ([a037d0c](https://github.com/antares-sql/antares/commit/a037d0cc0148444e8e6c5b87c79f6ba9c2a6f0fe))
* **UI:** BaseSelect option list scrolls automatically using up/down keys ([0043d07](https://github.com/antares-sql/antares/commit/0043d077081fc49724722a5d5a74986d990c539d))
* **UI:** BaseSelect small variant ([5582a12](https://github.com/antares-sql/antares/commit/5582a12bbfade75dbcc7f9d71ada7190ed08d3c2))
* **UI:** BaseSelect supports disabled options ([f7e04d6](https://github.com/antares-sql/antares/commit/f7e04d633340a53420ce1c434e906c9434620e6e))
* **UI:** BaseSelect supports option groups ([1869e6a](https://github.com/antares-sql/antares/commit/1869e6a1482daf9381d9ac2244bf0aeffa758edc))
* **UI:** ForeignKeySelect implements BaseSelect component ([302c664](https://github.com/antares-sql/antares/commit/302c66457deeb50facf4735291640fcf48b78f66))
* **UI:** initial BaseSelect integration ([22622df](https://github.com/antares-sql/antares/commit/22622df2cfcb71054c6f6110b7ad9d4f635553dc))
* **UI:** new BaseSelect component ([745d551](https://github.com/antares-sql/antares/commit/745d551cc9253eae4e39e5d3406ceee051a7d6c1))
* **UI:** select tab replace with BaseSelect component ([42bc919](https://github.com/antares-sql/antares/commit/42bc9196ffc2f64b77f9cb42136255fc74815034))
### Bug Fixes
* **PostgreSQL:** idle timeout disabled ([a082514](https://github.com/antares-sql/antares/commit/a082514f88040c7e0ffdf4e8357bab45370a4c39))
* query tab content disappears reordering or closing other tabs, closes [#261](https://github.com/antares-sql/antares/issues/261) ([c5baf2b](https://github.com/antares-sql/antares/commit/c5baf2b0d379fdd28ee8cb907628bbfca940e2f6))
* SSH tunnel connection error with private key, closes [#260](https://github.com/antares-sql/antares/issues/260) ([c826888](https://github.com/antares-sql/antares/commit/c826888b0dd0908958a4f727ddfa642e846269cf))
* **UI:** BaseSelect keyboard navigation ([7c45203](https://github.com/antares-sql/antares/commit/7c452036368fa0db6b9cde7c35e60a8e57bfece7))
* **UI:** BaseSelect style ([71b0736](https://github.com/antares-sql/antares/commit/71b0736d0ddbd599ab41cde0a6b0823e2bb7da2f))
### Improvements
* **UI:** max height for query text area increased ([5d5f1da](https://github.com/antares-sql/antares/commit/5d5f1da97b9adfa743197d8fa0bbb6addd565a7a))
### [0.5.4](https://github.com/antares-sql/antares/compare/v0.5.3...v0.5.4) (2022-05-10)
### Bug Fixes
* app blocked by BIT fields with no default, closes [#256](https://github.com/antares-sql/antares/issues/256) ([e62f280](https://github.com/antares-sql/antares/commit/e62f280528edb0ff4550ee75038ea216e81e4f10))
* file upload input not working ([58611bf](https://github.com/antares-sql/antares/commit/58611bf07f343e6899a7446bfcd1247b0c75fc7f))
* SSH tunnel not working ([6d61518](https://github.com/antares-sql/antares/commit/6d6151814e5006935d493b9b83dbda1aa5b35391))
* unable to insert auto-generated datetime fields ([ff27244](https://github.com/antares-sql/antares/commit/ff272440bdc2a7fe699e04f8809bd5af8f9529c0))
### Improvements
* **UI:** left alignment for numbers in result tables, closes [#249](https://github.com/antares-sql/antares/issues/249) ([e02565c](https://github.com/antares-sql/antares/commit/e02565c0d9bb63efa76a79f38e3ed3586a30ad1c))
### [0.5.3](https://github.com/antares-sql/antares/compare/v0.5.2...v0.5.3) (2022-05-08) ### [0.5.3](https://github.com/antares-sql/antares/compare/v0.5.2...v0.5.3) (2022-05-08)

133
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,133 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
fabio286@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -136,6 +136,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/raliqala"><img src="https://avatars.githubusercontent.com/u/30502407?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Topollo</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=raliqala" title="Code">💻</a></td> <td align="center"><a href="https://github.com/raliqala"><img src="https://avatars.githubusercontent.com/u/30502407?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Topollo</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=raliqala" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SmileYzn"><img src="https://avatars.githubusercontent.com/u/5851851?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cleverson</b></sub></a><br /><a href="#translation-SmileYzn" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/SmileYzn"><img src="https://avatars.githubusercontent.com/u/5851851?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cleverson</b></sub></a><br /><a href="#translation-SmileYzn" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/fredatgithub"><img src="https://avatars.githubusercontent.com/u/6720055?v=4?s=100" width="100px;" alt=""/><br /><sub><b>fred</b></sub></a><br /><a href="#translation-fredatgithub" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/fredatgithub"><img src="https://avatars.githubusercontent.com/u/6720055?v=4?s=100" width="100px;" alt=""/><br /><sub><b>fred</b></sub></a><br /><a href="#translation-fredatgithub" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/xak666"><img src="https://avatars.githubusercontent.com/u/38811437?v=4?s=100" width="100px;" alt=""/><br /><sub><b>xaka_xak</b></sub></a><br /><a href="#translation-xak666" title="Translation">🌍</a></td>
</tr> </tr>
</table> </table>

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

30996
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.5.3", "version": "0.5.8",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.", "description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/antares-sql/antares.git", "repository": "https://github.com/antares-sql/antares.git",
@@ -13,7 +13,7 @@
"compile:workers": "webpack --mode=production --config webpack.workers.config.js", "compile:workers": "webpack --mode=production --config webpack.workers.config.js",
"compile:renderer": "webpack --mode=production --config webpack.renderer.config.js", "compile:renderer": "webpack --mode=production --config webpack.renderer.config.js",
"build": "cross-env NODE_ENV=production npm run compile", "build": "cross-env NODE_ENV=production npm run compile",
"build:local": "npm run build && electron-builder", "build:local": "npm run build && electron-builder --publish never",
"build:appx": "npm run build:local -- --win appx", "build:appx": "npm run build:local -- --win appx",
"rebuild:electron": "rimraf ./dist && npm run postinstall", "rebuild:electron": "rimraf ./dist && npm run postinstall",
"release": "standard-version", "release": "standard-version",
@@ -22,8 +22,8 @@
"postinstall": "electron-builder install-app-deps && npm run devtools:install", "postinstall": "electron-builder install-app-deps && npm run devtools:install",
"test:e2e": "npm run compile && npm run test:e2e-dry", "test:e2e": "npm run compile && npm run test:e2e-dry",
"test:e2e-dry": "xvfb-maybe -- playwright test", "test:e2e-dry": "xvfb-maybe -- playwright test",
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"", "lint": "eslint . --ext .js,.ts,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix", "lint:fix": "eslint . --ext .js,.ts,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"contributors:add": "all-contributors add", "contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate" "contributors:generate": "all-contributors generate"
}, },
@@ -82,6 +82,12 @@
"license": "./LICENSE", "license": "./LICENSE",
"category": "Development" "category": "Development"
}, },
"nsis": {
"license": "./LICENSE",
"installerIcon": "assets/icon.ico",
"uninstallerIcon": "assets/icon.ico",
"installerHeader": "assets/icon.ico"
},
"portable": { "portable": {
"artifactName": "${productName}-${version}-portable.exe" "artifactName": "${productName}-${version}-portable.exe"
}, },
@@ -118,7 +124,7 @@
"better-sqlite3": "~7.5.0", "better-sqlite3": "~7.5.0",
"electron-log": "~4.4.1", "electron-log": "~4.4.1",
"electron-store": "~8.0.1", "electron-store": "~8.0.1",
"electron-updater": "~5.0.1", "electron-updater": "~4.6.5",
"electron-window-state": "~5.0.3", "electron-window-state": "~5.0.3",
"encoding": "~0.1.13", "encoding": "~0.1.13",
"leaflet": "~1.7.1", "leaflet": "~1.7.1",
@@ -144,6 +150,8 @@
"@babel/preset-typescript": "~7.16.7", "@babel/preset-typescript": "~7.16.7",
"@playwright/test": "~1.21.1", "@playwright/test": "~1.21.1",
"@types/better-sqlite3": "~7.5.0", "@types/better-sqlite3": "~7.5.0",
"@types/leaflet": "~1.7.9",
"@types/marked": "~4.0.3",
"@types/node": "~17.0.23", "@types/node": "~17.0.23",
"@types/pg": "~8.6.5", "@types/pg": "~8.6.5",
"@typescript-eslint/eslint-plugin": "~5.18.0", "@typescript-eslint/eslint-plugin": "~5.18.0",
@@ -154,7 +162,7 @@
"chalk": "~4.1.2", "chalk": "~4.1.2",
"cross-env": "~7.0.2", "cross-env": "~7.0.2",
"css-loader": "~6.5.0", "css-loader": "~6.5.0",
"electron": "~17.4.3", "electron": "~19.0.5",
"electron-builder": "~23.0.3", "electron-builder": "~23.0.3",
"eslint": "~7.32.0", "eslint": "~7.32.0",
"eslint-config-standard": "~16.0.3", "eslint-config-standard": "~16.0.3",
@@ -183,7 +191,7 @@
"unzip-crx-3": "~0.2.0", "unzip-crx-3": "~0.2.0",
"vue-eslint-parser": "~8.3.0", "vue-eslint-parser": "~8.3.0",
"vue-loader": "~16.8.3", "vue-loader": "~16.8.3",
"webpack": "~5.60.0", "webpack": "~5.72.0",
"webpack-cli": "~4.9.1", "webpack-cli": "~4.9.1",
"webpack-dev-server": "~4.4.0", "webpack-dev-server": "~4.4.0",
"xvfb-maybe": "~0.2.1" "xvfb-maybe": "~0.2.1"

View File

@@ -59,7 +59,7 @@ async function restartElectron () {
console.error(chalk.red(data.toString())); console.error(chalk.red(data.toString()));
}); });
electronProcess.on('exit', (code, signal) => { electronProcess.on('exit', () => {
if (!manualRestart) process.exit(0); if (!manualRestart) process.exit(0);
}); });
} }

View File

@@ -1,4 +1,6 @@
module.exports = { import { Customizations } from '../interfaces/customizations';
export const defaults: Customizations = {
// Defaults // Defaults
defaultPort: null, defaultPort: null,
defaultUser: null, defaultUser: null,
@@ -29,6 +31,7 @@ module.exports = {
elementsWrapper: '', elementsWrapper: '',
stringsWrapper: '"', stringsWrapper: '"',
tableAdd: false, tableAdd: false,
tableTruncateDisableFKCheck: false,
viewAdd: false, viewAdd: false,
triggerAdd: false, triggerAdd: false,
triggerFunctionAdd: false, triggerFunctionAdd: false,
@@ -68,24 +71,24 @@ module.exports = {
viewUpdateOption: false, viewUpdateOption: false,
procedureDeterministic: false, procedureDeterministic: false,
procedureDataAccess: false, procedureDataAccess: false,
procedureSql: false, procedureSql: null,
procedureContext: false, procedureContext: false,
procedureLanguage: false, procedureLanguage: false,
functionDeterministic: false, functionDeterministic: false,
functionDataAccess: false, functionDataAccess: false,
functionSql: false, functionSql: null,
functionContext: false, functionContext: false,
functionLanguage: false, functionLanguage: false,
triggerSql: false, triggerSql: null,
triggerStatementInCreation: false, triggerStatementInCreation: false,
triggerMultipleEvents: false, triggerMultipleEvents: false,
triggerTableInName: false, triggerTableInName: false,
triggerUpdateColumns: false, triggerUpdateColumns: false,
triggerOnlyRename: false, triggerOnlyRename: false,
triggerEnableDisable: false, triggerEnableDisable: false,
triggerFunctionSql: false, triggerFunctionSql: null,
triggerFunctionlanguages: false, triggerFunctionlanguages: null,
parametersLength: false, parametersLength: false,
languages: false, languages: null,
readOnlyMode: false readOnlyMode: false
}; };

View File

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

View File

@@ -0,0 +1,16 @@
import * as mysql from 'common/customizations/mysql';
import * as postgresql from 'common/customizations/postgresql';
import * as sqlite from 'common/customizations/sqlite';
import { Customizations } from 'common/interfaces/customizations';
export default {
maria: mysql.customizations,
mysql: mysql.customizations,
pg: postgresql.customizations,
sqlite: sqlite.customizations
} as {
maria: Customizations;
mysql: Customizations;
pg: Customizations;
sqlite: Customizations;
};

View File

@@ -1,6 +1,7 @@
const defaults = require('./defaults'); import { Customizations } from '../interfaces/customizations';
import { defaults } from './defaults';
module.exports = { export const customizations: Customizations = {
...defaults, ...defaults,
// Defaults // Defaults
defaultPort: 3306, defaultPort: 3306,
@@ -27,6 +28,7 @@ module.exports = {
elementsWrapper: '', elementsWrapper: '',
stringsWrapper: '"', stringsWrapper: '"',
tableAdd: true, tableAdd: true,
tableTruncateDisableFKCheck: true,
viewAdd: true, viewAdd: true,
triggerAdd: true, triggerAdd: true,
routineAdd: true, routineAdd: true,

View File

@@ -1,6 +1,7 @@
const defaults = require('./defaults'); import { Customizations } from '../interfaces/customizations';
import { defaults } from './defaults';
module.exports = { export const customizations: Customizations = {
...defaults, ...defaults,
// Defaults // Defaults
defaultPort: 5432, defaultPort: 5432,

View File

@@ -1,4 +1,8 @@
module.exports = { import { Customizations } from '../interfaces/customizations';
import { defaults } from './defaults';
export const customizations: Customizations = {
...defaults,
// Core // Core
fileConnection: true, fileConnection: true,
// Structure // Structure

View File

@@ -1,4 +1,6 @@
module.exports = [ import { TypesGroup } from 'common/interfaces/antares';
export default [
{ {
group: 'integer', group: 'integer',
types: [ types: [
@@ -306,4 +308,4 @@ module.exports = [
} }
] ]
} }
]; ] as TypesGroup[];

View File

@@ -1,4 +1,6 @@
module.exports = [ import { TypesGroup } from 'common/interfaces/antares';
export default [
{ {
group: 'integer', group: 'integer',
types: [ types: [
@@ -290,4 +292,4 @@ module.exports = [
} }
] ]
} }
]; ] as TypesGroup[];

View File

@@ -1,4 +1,6 @@
module.exports = [ import { TypesGroup } from 'common/interfaces/antares';
export default [
{ {
group: 'integer', group: 'integer',
types: [ types: [
@@ -134,4 +136,4 @@ module.exports = [
} }
] ]
} }
]; ] as TypesGroup[];

View File

@@ -1,4 +1,4 @@
module.exports = [ export default [
'PRIMARY', 'PRIMARY',
'INDEX', 'INDEX',
'UNIQUE', 'UNIQUE',

View File

@@ -1,4 +1,4 @@
module.exports = [ export default [
'PRIMARY', 'PRIMARY',
'INDEX', 'INDEX',
'UNIQUE' 'UNIQUE'

View File

@@ -1,4 +1,4 @@
module.exports = [ export default [
'PRIMARY', 'PRIMARY',
'INDEX', 'INDEX',
'UNIQUE' 'UNIQUE'

View File

@@ -14,6 +14,12 @@ export type ClientCode = 'mysql' | 'maria' | 'pg' | 'sqlite'
export type Exporter = MysqlExporter | PostgreSQLExporter export type Exporter = MysqlExporter | PostgreSQLExporter
export type Importer = MySQLImporter | PostgreSQLImporter export type Importer = MySQLImporter | PostgreSQLImporter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface IpcResponse<T = any> {
status: 'success' | 'error';
response?: T;
}
/** /**
* Pasameters needed to create a new Antares connection to a database * Pasameters needed to create a new Antares connection to a database
*/ */
@@ -67,12 +73,34 @@ export interface TypeInformations {
zerofill: boolean; zerofill: boolean;
} }
export interface TypesGroup {
group: string;
types: TypeInformations[];
}
// Tables // Tables
export interface TableField { export interface TableInfos {
name: string;
type: string;
rows: number;
created: Date;
updated: Date;
engine: string;
comment: string;
size: number | false;
autoIncrement: number;
collation: string;
}
export type TableOptions = Partial<TableInfos>;
export interface TableField {
// eslint-disable-next-line camelcase
_antares_id?: string;
name: string; name: string;
key: string;
type: string; type: string;
schema: string; schema: string;
table?: string;
numPrecision?: number; numPrecision?: number;
numLength?: number; numLength?: number;
datePrecision?: number; datePrecision?: number;
@@ -82,7 +110,8 @@ export interface TableField {
unsigned?: boolean; unsigned?: boolean;
zerofill?: boolean; zerofill?: boolean;
order?: number; order?: number;
default?: number | string; default?: string;
defaultType?: string;
enumValues?: string; enumValues?: string;
charset?: string; charset?: string;
collation?: string; collation?: string;
@@ -92,9 +121,16 @@ export interface TableField {
comment?: string; comment?: string;
after?: string; after?: string;
orgName?: string; orgName?: string;
length?: number | false;
alias: string;
tableAlias: string;
orgTable: string;
key?: 'pri' | 'uni' | '';
} }
export interface TableIndex { export interface TableIndex {
// eslint-disable-next-line camelcase
_antares_id?: string;
name: string; name: string;
fields: string[]; fields: string[];
type: string; type: string;
@@ -107,6 +143,8 @@ export interface TableIndex {
} }
export interface TableForeign { export interface TableForeign {
// eslint-disable-next-line camelcase
_antares_id?: string;
constraintName: string; constraintName: string;
refSchema: string; refSchema: string;
table: string; table: string;
@@ -118,15 +156,6 @@ export interface TableForeign {
oldName?: string; oldName?: string;
} }
export interface TableOptions {
name: string;
type?: 'table' | 'view';
engine?: string;
comment?: string;
collation?: string;
autoIncrement?: number;
}
export interface CreateTableParams { export interface CreateTableParams {
/** Connection UID */ /** Connection UID */
uid?: string; uid?: string;
@@ -165,6 +194,7 @@ export interface AlterTableParams {
} }
// Views // Views
export type ViewInfos = TableInfos
export interface CreateViewParams { export interface CreateViewParams {
schema: string; schema: string;
name: string; name: string;
@@ -180,6 +210,19 @@ export interface AlterViewParams extends CreateViewParams {
} }
// Triggers // Triggers
export interface TriggerInfos {
name: string;
statement: string;
timing: string;
definer: string;
event: string;
table: string;
sqlMode: string;
created: Date;
charset: string;
enabled?: boolean;
}
export interface CreateTriggerParams { export interface CreateTriggerParams {
definer?: string; definer?: string;
schema: string; schema: string;
@@ -195,13 +238,38 @@ export interface AlterTriggerParams extends CreateTriggerParams {
} }
// Routines & Functions // Routines & Functions
export interface FunctionParam { export interface FunctionParam {
// eslint-disable-next-line camelcase
_antares_id: string;
context: string; context: string;
name: string; name: string;
type: string; type: string;
length: number; length: number;
} }
export interface RoutineInfos {
name: string;
type?: string;
definer: string;
created?: string;
sql?: string;
updated?: string;
comment?: string;
charset?: string;
security?: string;
language?: string;
dataAccess?: string;
deterministic?: boolean;
parameters?: FunctionParam[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
returns?: any;
returnsLength?: number;
}
export type FunctionInfos = RoutineInfos
export type TriggerFunctionInfos = FunctionInfos
export interface CreateRoutineParams { export interface CreateRoutineParams {
name: string; name: string;
parameters?: FunctionParam[]; parameters?: FunctionParam[];
@@ -239,7 +307,7 @@ export interface AlterFunctionParams extends CreateFunctionParams {
} }
// Events // Events
export interface CreateEventParams { export interface EventInfos {
definer?: string; definer?: string;
schema: string; schema: string;
name: string; name: string;
@@ -248,16 +316,39 @@ export interface CreateEventParams {
starts: string; starts: string;
ends: string; ends: string;
at: string; at: string;
preserve: string; preserve: boolean;
state: string; state: string;
comment: string; comment: string;
enabled?: boolean;
sql: string; sql: string;
} }
export type CreateEventParams = EventInfos;
export interface AlterEventParams extends CreateEventParams { export interface AlterEventParams extends CreateEventParams {
oldName?: string; oldName?: string;
} }
// Schema
export interface SchemaInfos {
name: string;
size: number;
tables: TableInfos[];
functions: FunctionInfos[];
procedures: RoutineInfos[];
triggers: TriggerInfos[];
schedulers: EventInfos[];
}
export interface CollationInfos {
charset: string;
collation: string;
compiled: boolean;
default: boolean;
id: string | number;
sortLen: number;
}
// Query // Query
export interface QueryBuilderObject { export interface QueryBuilderObject {
schema: string; schema: string;
@@ -285,17 +376,10 @@ export interface QueryParams {
tabUid?: string; tabUid?: string;
} }
export interface QueryField { /**
name: string; * @deprecated Use TableFIeld
alias: string; */
orgName: string; export type QueryField = TableField
schema: string;
table: string;
tableAlias: string;
orgTable: string;
type: string;
length: number;
}
export interface QueryForeign { export interface QueryForeign {
schema: string; schema: string;

View File

@@ -0,0 +1,92 @@
export interface Customizations {
// Defaults
defaultPort?: number;
defaultUser?: string;
defaultDatabase?: string;
// Core
database?: boolean;
collations?: boolean;
engines?: boolean;
connectionSchema?: boolean;
sslConnection?: boolean;
sshConnection?: boolean;
fileConnection?: boolean;
cancelQueries?: boolean;
// Tools
processesList?: boolean;
usersManagement?: boolean;
variables?: boolean;
// Structure
schemas?: boolean;
tables?: boolean;
views?: boolean;
triggers?: boolean;
triggerFunctions?: boolean;
routines?: boolean;
functions?: boolean;
schedulers?: boolean;
// Settings
elementsWrapper: string;
stringsWrapper: string;
tableAdd?: boolean;
tableSettings?: boolean;
tableOptions?: boolean;
tableArray?: boolean;
tableRealCount?: boolean;
tableTruncateDisableFKCheck?: boolean;
viewAdd?: boolean;
viewSettings?: boolean;
triggerAdd?: boolean;
triggerFunctionAdd?: boolean;
routineAdd?: boolean;
functionAdd?: boolean;
schedulerAdd?: boolean;
databaseEdit?: boolean;
schemaEdit?: boolean;
schemaDrop?: boolean;
schemaExport?: boolean;
exportByChunks?: boolean;
schemaImport?: boolean;
triggerSettings?: boolean;
triggerFunctionSettings?: boolean;
routineSettings?: boolean;
functionSettings?: boolean;
schedulerSettings?: boolean;
indexes?: boolean;
foreigns?: boolean;
sortableFields?: boolean;
unsigned?: boolean;
nullable?: boolean;
nullablePrimary?: boolean;
zerofill?: boolean;
autoIncrement?: boolean;
comment?: boolean;
collation?: boolean;
definer?: boolean;
onUpdate?: boolean;
viewAlgorithm?: boolean;
viewSqlSecurity?: boolean;
viewUpdateOption?: boolean;
procedureDeterministic?: boolean;
procedureDataAccess?: boolean;
procedureSql?: string;
procedureContext?: boolean;
procedureLanguage?: boolean;
functionDeterministic?: boolean;
functionDataAccess?: boolean;
functionSql?: string;
functionContext?: boolean;
functionLanguage?: boolean;
triggerSql?: string;
triggerStatementInCreation?: boolean;
triggerMultipleEvents?: boolean;
triggerTableInName?: boolean;
triggerUpdateColumns?: boolean;
triggerOnlyRename?: boolean;
triggerEnableDisable?: boolean;
triggerFunctionSql?: string;
triggerFunctionlanguages?: string[];
parametersLength?: boolean;
languages?: string[];
readOnlyMode?: boolean;
}

View File

@@ -7,13 +7,7 @@ export interface TableParams {
export interface ExportOptions { export interface ExportOptions {
schema: string; schema: string;
includes: { includes: {[key: string]: boolean};
functions: boolean;
views: boolean;
triggers: boolean;
routines: boolean;
schedulers: boolean;
};
outputFormat: 'sql' | 'sql.zip'; outputFormat: 'sql' | 'sql.zip';
outputFile: string; outputFile: string;
sqlInsertAfter: number; sqlInsertAfter: number;

View File

@@ -1,5 +1,34 @@
import { UsableLocale } from '@faker-js/faker'; import { UsableLocale } from '@faker-js/faker';
export interface TableUpdateParams {
uid: string;
schema: string;
table: string;
primary?: string;
id: number | string;
content: number | string | boolean | Date | Blob | null;
type: string;
field: string;
}
export interface TableDeleteParams {
uid: string;
schema: string;
table: string;
primary?: string;
field: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rows: {[key: string]: any};
}
export interface TableFilterClausole {
active: boolean;
field: string;
op: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'IN' | 'NOT IN' | 'LIKE' | 'BETWEEN' | 'IS NULL' | 'IS NOT NULL';
value: '';
value2: '';
}
export interface InsertRowsParams { export interface InsertRowsParams {
uid: string; uid: string;
schema: string; schema: string;

View File

@@ -1,7 +1,6 @@
'use strict'; export function bufferToBase64 (buf: Buffer) {
export function bufferToBase64 (buf) {
const binstr = Array.prototype.map.call(buf, ch => { const binstr = Array.prototype.map.call(buf, ch => {
return String.fromCharCode(ch); return String.fromCharCode(ch);
}).join(''); }).join('');
return btoa(binstr); return Buffer.from(binstr, 'binary').toString('base64');
} }

View File

@@ -1,5 +1,4 @@
'use strict'; export function formatBytes (bytes: number, decimals = 2) {
export function formatBytes (bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes';
const k = 1024; const k = 1024;

View File

@@ -1,10 +0,0 @@
/**
*
* @param {any[]} array
* @returns {number}
*/
export function getArrayDepth (array) {
return Array.isArray(array)
? 1 + Math.max(0, ...array.map(getArrayDepth))
: 0;
}

View File

@@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export function getArrayDepth (array: any[]): number {
return Array.isArray(array)
? 1 + Math.max(0, ...array.map(getArrayDepth))
: 0;
}

View File

@@ -1,5 +1,3 @@
'use strict';
const lookup = { const lookup = {
0: '0000', 0: '0000',
1: '0001', 1: '0001',
@@ -23,15 +21,11 @@ const lookup = {
D: '1101', D: '1101',
E: '1110', E: '1110',
F: '1111' F: '1111'
}; } as const;
/** export type HexChar = keyof typeof lookup
* Converts hexadecimal string to binary string
* export default function hexToBinary (hex: HexChar[]) {
* @param {string} hex Hexadecimal string
* @returns {string} Binary string
*/
export default function hexToBinary (hex) {
let binary = ''; let binary = '';
for (let i = 0; i < hex.length; i++) for (let i = 0; i < hex.length; i++)
binary += lookup[hex[i]]; binary += lookup[hex[i]];

View File

@@ -1,5 +1,4 @@
'use strict'; export function mimeFromHex (hex: string) {
export function mimeFromHex (hex) {
switch (hex.substring(0, 4)) { // 2 bytes switch (hex.substring(0, 4)) { // 2 bytes
case '424D': case '424D':
return { ext: 'bmp', mime: 'image/bmp' }; return { ext: 'bmp', mime: 'image/bmp' };
@@ -23,7 +22,7 @@ export function mimeFromHex (hex) {
case '425A68': case '425A68':
return { ext: 'bz2', mime: 'application/x-bzip2' }; return { ext: 'bz2', mime: 'application/x-bzip2' };
default: default:
switch (hex) { // 4 bytes switch (hex) { // 4 bites
case '89504E47': case '89504E47':
return { ext: 'png', mime: 'image/png' }; return { ext: 'png', mime: 'image/png' };
case '47494638': case '47494638':

View File

@@ -3,13 +3,7 @@
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm; const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
const regex = new RegExp(pattern); const regex = new RegExp(pattern);
/** function sqlEscaper (string: string) {
* Escapes a string
*
* @param {String} string
* @returns {String}
*/
function sqlEscaper (string) {
return string.replace(regex, char => { return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%']; const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%']; const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];

View File

@@ -1,8 +0,0 @@
/**
* @export
* @param {String} [prefix]
* @returns {String} Unique ID
*/
export function uidGen (prefix) {
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase();
}

View File

@@ -0,0 +1,3 @@
export function uidGen (prefix?: string) {
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase();
}

View File

@@ -5,11 +5,6 @@ export default () => {
app.exit(); app.exit();
}); });
ipcMain.on('get-key', async event => {
const key = false;
event.returnValue = key;
});
ipcMain.handle('show-open-dialog', (event, options) => { ipcMain.handle('show-open-dialog', (event, options) => {
return dialog.showOpenDialog(options); return dialog.showOpenDialog(options);
}); });

View File

@@ -1,5 +1,5 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import fs from 'fs'; import * as fs from 'fs';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { ClientsFactory } from '../libs/ClientsFactory'; import { ClientsFactory } from '../libs/ClientsFactory';
import { SslOptions } from 'mysql2'; import { SslOptions } from 'mysql2';

View File

@@ -172,7 +172,10 @@ export default (connections: {[key: string]: antares.Client}) => {
}); });
ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => { ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => {
if (exporter !== null) return; if (exporter !== null) {
exporter.kill();
return;
}
return new Promise((resolve/*, reject */) => { return new Promise((resolve/*, reject */) => {
(async () => { (async () => {
@@ -265,7 +268,10 @@ export default (connections: {[key: string]: antares.Client}) => {
}); });
ipcMain.handle('import-sql', async (event, options) => { ipcMain.handle('import-sql', async (event, options) => {
if (importer !== null) return; if (importer !== null) {
importer.kill();
return;
}
return new Promise((resolve/*, reject */) => { return new Promise((resolve/*, reject */) => {
(async () => { (async () => {

View File

@@ -1,12 +1,12 @@
import * as fs from 'fs';
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import { InsertRowsParams } from 'common/interfaces/tableApis'; import { InsertRowsParams } from 'common/interfaces/tableApis';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import moment from 'moment'; import * as moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper'; import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import * as customizations from 'common/customizations'; import customizations from 'common/customizations';
import fs from 'fs';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-table-columns', async (event, params) => { ipcMain.handle('get-table-columns', async (event, params) => {
@@ -104,8 +104,6 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = `"${sqlEscaper(params.content)}"`; escapedParam = `"${sqlEscaper(params.content)}"`;
break; break;
case 'pg': case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
case 'sqlite': case 'sqlite':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break; break;
@@ -171,6 +169,8 @@ export default (connections: {[key: string]: antares.Client}) => {
} }
else { else {
const { orgRow } = params; const { orgRow } = params;
delete orgRow._antares_id;
reload = true; reload = true;
for (const key in orgRow) { for (const key in orgRow) {
@@ -246,84 +246,12 @@ export default (connections: {[key: string]: antares.Client}) => {
} }
}); });
ipcMain.handle('insert-table-rows', async (event, params) => {
try { // TODO: move to client classes
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const insertObj: {[key: string]: any} = {};
for (const key in params.row) {
const type = params.fields[key];
let escapedParam;
if (params.row[key] === null)
escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = +params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break;
case 'pg':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (BLOB.includes(type)) {
if (params.row[key].value) {
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
}
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '""';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
}
insertObj[key] = escapedParam;
}
const rows = new Array(+params.repeat).fill(insertObj);
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(rows)
.run();
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('insert-table-fake-rows', async (event, params: InsertRowsParams) => { ipcMain.handle('insert-table-fake-rows', async (event, params: InsertRowsParams) => {
try { // TODO: move to client classes try { // TODO: move to client classes
// eslint-disable-next-line @typescript-eslint/no-explicit-any const rows: {[key: string]: string | number | boolean | Date | Buffer}[] = [];
const rows: {[key: string]: any}[] = [];
for (let i = 0; i < +params.repeat; i++) { for (let i = 0; i < +params.repeat; i++) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const insertObj: {[key: string]: string | number | boolean | Date | Buffer} = {};
const insertObj: {[key: string]: any} = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@@ -341,6 +269,7 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = `"${sqlEscaper(params.row[key].value)}"`; escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break; break;
case 'pg': case 'pg':
case 'sqlite':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break; break;
} }
@@ -381,8 +310,7 @@ export default (connections: {[key: string]: antares.Client}) => {
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
else { // Faker value else { // Faker value
// eslint-disable-next-line @typescript-eslint/no-explicit-any const parsedParams: {[key: string]: string | number | boolean | Date | Buffer} = {};
const parsedParams: {[key: string]: any} = {};
let fakeValue; let fakeValue;
if (params.locale) if (params.locale)
@@ -402,7 +330,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (typeof fakeValue === 'string') { if (typeof fakeValue === 'string') {
if (params.row[key].length) if (params.row[key].length)
fakeValue = fakeValue.substr(0, params.row[key].length); fakeValue = fakeValue.substring(0, params.row[key].length);
fakeValue = `'${sqlEscaper(fakeValue)}'`; fakeValue = `'${sqlEscaper(fakeValue)}'`;
} }
else if ([...DATE, ...DATETIME].includes(type)) else if ([...DATE, ...DATETIME].includes(type))

View File

@@ -1,7 +1,7 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import Store from 'electron-store'; import * as Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' }); const persistentStore = new Store({ name: 'settings', clearInvalidConfig: true });
const isMacOS = process.platform === 'darwin'; const isMacOS = process.platform === 'darwin';
let mainWindow: Electron.IpcMainEvent; let mainWindow: Electron.IpcMainEvent;

View File

@@ -1,8 +1,8 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as mysql from 'mysql2/promise'; import * as mysql from 'mysql2/promise';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import * as dataTypes from 'common/data-types/mysql'; import dataTypes from 'common/data-types/mysql';
import SSH2Promise from 'ssh2-promise'; import SSH2Promise = require('ssh2-promise');
import SSHConfig from 'ssh2-promise/lib/sshConfig'; import SSHConfig from 'ssh2-promise/lib/sshConfig';
export class MySQLClient extends AntaresCore { export class MySQLClient extends AntaresCore {
@@ -321,7 +321,7 @@ export class MySQLClient extends AntaresCore {
return filteredDatabases.map(db => { return filteredDatabases.map(db => {
if (schemas.has(db.Database)) { if (schemas.has(db.Database)) {
// TABLES // TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => { const remappedTables: antares.TableInfos[] = tablesArr.filter(table => table.Db === db.Database).map(table => {
let tableType; let tableType;
switch (table.Comment) { switch (table.Comment) {
case 'VIEW': case 'VIEW':
@@ -350,7 +350,7 @@ export class MySQLClient extends AntaresCore {
}); });
// PROCEDURES // PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => { const remappedProcedures: antares.RoutineInfos[] = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
return { return {
name: procedure.Name, name: procedure.Name,
type: procedure.Type, type: procedure.Type,
@@ -364,7 +364,7 @@ export class MySQLClient extends AntaresCore {
}); });
// FUNCTIONS // FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.Database).map(func => { const remappedFunctions: antares.FunctionInfos[] = functions.filter(func => func.Db === db.Database).map(func => {
return { return {
name: func.Name, name: func.Name,
type: func.Type, type: func.Type,
@@ -378,33 +378,26 @@ export class MySQLClient extends AntaresCore {
}); });
// SCHEDULERS // SCHEDULERS
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => { const remappedSchedulers: antares.EventInfos[] = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return { return {
name: scheduler.EVENT_NAME, name: scheduler.EVENT_NAME,
definition: scheduler.EVENT_DEFINITION, schema: scheduler.Db,
type: scheduler.EVENT_TYPE, sql: scheduler.EVENT_DEFINITION,
execution: scheduler.EVENT_TYPE === 'RECURRING' ? 'EVERY' : 'ONCE',
definer: scheduler.DEFINER, definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS, starts: scheduler.STARTS,
ends: scheduler.ENDS, ends: scheduler.ENDS,
state: scheduler.STATUS === 'ENABLED' ? 'ENABLE' : scheduler.STATE === 'DISABLED' ? 'DISABLE' : 'DISABLE ON SLAVE',
enabled: scheduler.STATUS === 'ENABLED', enabled: scheduler.STATUS === 'ENABLED',
executeAt: scheduler.EXECUTE_AT, at: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD, every: [scheduler.INTERVAL_FIELD, scheduler.INTERVAL_VALUE],
intervalValue: scheduler.INTERVAL_VALUE, preserve: scheduler.ON_COMPLETION.includes('PRESERVE'),
onCompletion: scheduler.ON_COMPLETION, comment: scheduler.EVENT_COMMENT
originator: scheduler.ORIGINATOR,
sqlMode: scheduler.SQL_MODE,
created: scheduler.CREATED,
updated: scheduler.LAST_ALTERED,
lastExecuted: scheduler.LAST_EXECUTED,
comment: scheduler.EVENT_COMMENT,
charset: scheduler.CHARACTER_SET_CLIENT,
timezone: scheduler.TIME_ZONE
}; };
}); });
// TRIGGERS // TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => { const remappedTriggers: antares.TriggerInfos[] = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => {
return { return {
name: trigger.Trigger, name: trigger.Trigger,
statement: trigger.Statement, statement: trigger.Statement,
@@ -919,8 +912,15 @@ export class MySQLClient extends AntaresCore {
return await this.raw(sql); return await this.raw(sql);
} }
async truncateTable (params: { schema: string; table: string }) { async truncateTable (params: { schema: string; table: string; force: boolean }) {
const sql = `TRUNCATE TABLE \`${params.schema}\`.\`${params.table}\``; let sql = `TRUNCATE TABLE \`${params.schema}\`.\`${params.table}\`;`;
if (params.force) {
sql = `
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
${sql}
SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1);
`;
}
return await this.raw(sql); return await this.raw(sql);
} }
@@ -930,19 +930,22 @@ export class MySQLClient extends AntaresCore {
} }
async getViewInformations ({ schema, view }: { schema: string; view: string }) { async getViewInformations ({ schema, view }: { schema: string; view: string }) {
const sql = `SHOW CREATE VIEW \`${schema}\`.\`${view}\``; const { rows: algorithm } = await this.raw(`SHOW CREATE VIEW \`${schema}\`.\`${view}\``);
const results = await this.raw(sql); const { rows: viewInfo } = await this.raw(`
SELECT *
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA = '${schema}'
AND TABLE_NAME = '${view}'
`);
return results.rows.map(row => { return {
return { algorithm: algorithm[0]['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0],
algorithm: row['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0], definer: viewInfo[0].DEFINER,
definer: row['Create View'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], security: viewInfo[0].SECURITY_TYPE,
security: row['Create View'].match(/(?<=SQL SECURITY ).*?(?=\s)/gs)[0], updateOption: viewInfo[0].CHECK_OPTION === 'NONE' ? '' : viewInfo[0].CHECK_OPTION,
updateOption: row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs) ? row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs)[0] : '', sql: viewInfo[0].VIEW_DEFINITION,
sql: row['Create View'].match(/(?<=AS ).*?$/gs)[0], name: viewInfo[0].TABLE_NAME
name: row.View };
};
})[0];
} }
async dropView (params: { schema: string; view: string }) { async dropView (params: { schema: string; view: string }) {
@@ -955,7 +958,7 @@ export class MySQLClient extends AntaresCore {
USE \`${view.schema}\`; USE \`${view.schema}\`;
ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''}
SQL SECURITY ${view.security} SQL SECURITY ${view.security}
params \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''} VIEW \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}
`; `;
if (view.name !== view.oldName) if (view.name !== view.oldName)
@@ -1381,6 +1384,14 @@ export class MySQLClient extends AntaresCore {
xa: row.XA, xa: row.XA,
savepoints: row.Savepoints, savepoints: row.Savepoints,
isDefault: row.Support.includes('DEFAULT') isDefault: row.Support.includes('DEFAULT')
} as {
name: string;
support: string;
comment: string;
transactions: string;
xa: string;
savepoints: string;
isDefault: boolean;
}; };
}); });
} }
@@ -1405,7 +1416,12 @@ export class MySQLClient extends AntaresCore {
break; break;
} }
return acc; return acc;
}, {}); }, {}) as {
number: string;
name: string;
arch: string;
os: string;
};
} }
async getProcesses () { async getProcesses () {
@@ -1423,6 +1439,15 @@ export class MySQLClient extends AntaresCore {
time: row.TIME, time: row.TIME,
state: row.STATE, state: row.STATE,
info: row.INFO info: row.INFO
} as {
id: number;
user: string;
host: string;
db: string;
command: string;
time: number;
state: string;
info: string;
}; };
}); });
} }

View File

@@ -4,8 +4,8 @@ import { builtinsTypes } from 'pg-types';
import * as pg from 'pg'; import * as pg from 'pg';
import * as pgAst from 'pgsql-ast-parser'; import * as pgAst from 'pgsql-ast-parser';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import * as dataTypes from 'common/data-types/postgresql'; import dataTypes from 'common/data-types/postgresql';
import SSH2Promise from 'ssh2-promise'; import SSH2Promise = require('ssh2-promise');
import SSHConfig from 'ssh2-promise/lib/sshConfig'; import SSHConfig from 'ssh2-promise/lib/sshConfig';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -144,7 +144,11 @@ export class PostgreSQLClient extends AntaresCore {
async getConnectionPool () { async getConnectionPool () {
const dbConfig = await this.getDbConfig(); const dbConfig = await this.getDbConfig();
const pool = new pg.Pool({ ...dbConfig, max: this._poolSize }); const pool = new pg.Pool({
...dbConfig,
max: this._poolSize,
idleTimeoutMillis: 0
});
const connection = pool; const connection = pool;
if (this._params.readonly) { if (this._params.readonly) {

View File

@@ -1,7 +1,7 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as sqlite from 'better-sqlite3'; import * as sqlite from 'better-sqlite3';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import * as dataTypes from 'common/data-types/sqlite'; import dataTypes from 'common/data-types/sqlite';
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes'; import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
export class SQLiteClient extends AntaresCore { export class SQLiteClient extends AntaresCore {

View File

@@ -2,7 +2,7 @@ import * as exporter from 'common/interfaces/exporter';
import * as mysql from 'mysql2/promise'; import * as mysql from 'mysql2/promise';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes'; import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary, { HexChar } from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth'; import { getArrayDepth } from 'common/libs/getArrayDepth';
import * as moment from 'moment'; import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers'; import { lineString, point, polygon } from '@turf/helpers';
@@ -138,7 +138,7 @@ ${footer}
: this.escapeAndQuote(val); : this.escapeAndQuote(val);
} }
else if (BIT.includes(column.type)) else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`; sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(column.type)) else if (BLOB.includes(column.type))
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`; sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (NUMBER.includes(column.type)) else if (NUMBER.includes(column.type))

View File

@@ -2,7 +2,7 @@ import * as antares from 'common/interfaces/antares';
import * as exporter from 'common/interfaces/exporter'; import * as exporter from 'common/interfaces/exporter';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes'; import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary, { HexChar } from 'common/libs/hexToBinary';
import * as moment from 'moment'; import * as moment from 'moment';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
@@ -249,7 +249,7 @@ SET row_security = off;\n\n\n`;
else if (TEXT_SEARCH.includes(column.type)) else if (TEXT_SEARCH.includes(column.type))
sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`; sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(column.type)) else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`; sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(column.type)) else if (BLOB.includes(column.type))
sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`; sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
else if (NUMBER.includes(column.type)) else if (NUMBER.includes(column.type))

View File

@@ -1,6 +1,6 @@
import * as pg from 'pg'; import * as pg from 'pg';
import * as importer from 'common/interfaces/importer'; import * as importer from 'common/interfaces/importer';
import fs from 'fs/promises'; import * as fs from 'fs/promises';
import PostgreSQLParser from '../../parsers/PostgreSQLParser'; import PostgreSQLParser from '../../parsers/PostgreSQLParser';
import { BaseImporter } from '../BaseImporter'; import { BaseImporter } from '../BaseImporter';

View File

@@ -1,4 +1,4 @@
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron'; import { app, BrowserWindow, /* session, */ nativeImage, Menu, ipcMain } from 'electron';
import * as path from 'path'; import * as path from 'path';
import * as Store from 'electron-store'; import * as Store from 'electron-store';
import * as windowStateKeeper from 'electron-window-state'; import * as windowStateKeeper from 'electron-window-state';
@@ -7,9 +7,13 @@ import * as remoteMain from '@electron/remote/main';
import ipcHandlers from './ipc-handlers'; import ipcHandlers from './ipc-handlers';
Store.initRenderer(); Store.initRenderer();
const persistentStore = new Store({ name: 'settings' });
const appTheme = persistentStore.get('application_theme');
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
const isMacOS = process.platform === 'darwin'; const isMacOS = process.platform === 'darwin';
const isLinux = process.platform === 'linux';
const isWindows = process.platform === 'win32';
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'; process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
@@ -19,7 +23,7 @@ let mainWindow: BrowserWindow;
let mainWindowState: windowStateKeeper.State; let mainWindowState: windowStateKeeper.State;
async function createMainWindow () { async function createMainWindow () {
const icon = require('../renderer/images/logo-32.png'); const icon = require('../renderer/images/logo-64.png');
const window = new BrowserWindow({ const window = new BrowserWindow({
width: mainWindowState.width, width: mainWindowState.width,
height: mainWindowState.height, height: mainWindowState.height,
@@ -27,16 +31,23 @@ async function createMainWindow () {
y: mainWindowState.y, y: mainWindowState.y,
minWidth: 900, minWidth: 900,
minHeight: 550, minHeight: 550,
show: !isWindows,
title: 'Antares SQL', title: 'Antares SQL',
autoHideMenuBar: true,
icon: nativeImage.createFromDataURL(icon.default), icon: nativeImage.createFromDataURL(icon.default),
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false, contextIsolation: false,
spellcheck: false spellcheck: false
}, },
frame: false, autoHideMenuBar: true,
titleBarStyle: isMacOS ? 'hidden' : 'default', titleBarStyle: isLinux ? 'default' :'hidden',
titleBarOverlay: isWindows
? {
color: appTheme === 'dark' ? '#3f3f3f' : '#fff',
symbolColor: appTheme === 'dark' ? '#fff' : '#000',
height: 30
}
: false,
trafficLightPosition: isMacOS ? { x: 10, y: 8 } : undefined, trafficLightPosition: isMacOS ? { x: 10, y: 8 } : undefined,
backgroundColor: '#1d1d1d' backgroundColor: '#1d1d1d'
}); });
@@ -73,10 +84,24 @@ else {
// Initialize ipcHandlers // Initialize ipcHandlers
ipcHandlers(); ipcHandlers();
ipcMain.on('refresh-theme-settings', () => {
const appTheme = persistentStore.get('application_theme');
if (isWindows && mainWindow) {
mainWindow.setTitleBarOverlay({
color: appTheme === 'dark' ? '#3f3f3f' : '#fff',
symbolColor: appTheme === 'dark' ? '#fff' : '#000'
});
}
});
ipcMain.on('change-window-title', (_, title: string) => {
if (mainWindow) mainWindow.setTitle(title);
});
// quit application when all windows are closed // quit application when all windows are closed
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits // on macOS it is common for applications to stay open until the user explicitly quits
if (isMacOS) app.quit(); if (!isMacOS) app.quit();
}); });
app.on('activate', async () => { app.on('activate', async () => {
@@ -95,6 +120,9 @@ else {
mainWindow = await createMainWindow(); mainWindow = await createMainWindow();
createAppMenu(); createAppMenu();
if (isWindows)
mainWindow.show();
if (isDevelopment) if (isDevelopment)
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();

View File

@@ -24,7 +24,7 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
@@ -33,20 +33,20 @@ import { useApplicationStore } from '@/stores/application';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import TheSettingBar from '@/components/TheSettingBar'; import TheSettingBar from '@/components/TheSettingBar.vue';
export default { export default {
name: 'App', name: 'App',
components: { components: {
TheTitleBar: defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar')), TheTitleBar: defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar.vue')),
TheSettingBar, TheSettingBar,
TheFooter: defineAsyncComponent(() => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter')), TheFooter: defineAsyncComponent(() => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter.vue')),
TheNotificationsBoard: defineAsyncComponent(() => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard')), TheNotificationsBoard: defineAsyncComponent(() => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard.vue')),
Workspace: defineAsyncComponent(() => import(/* webpackChunkName: "Workspace" */'@/components/Workspace')), Workspace: defineAsyncComponent(() => import(/* webpackChunkName: "Workspace" */'@/components/Workspace.vue')),
WorkspaceAddConnectionPanel: defineAsyncComponent(() => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel')), WorkspaceAddConnectionPanel: defineAsyncComponent(() => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel.vue')),
ModalSettings: defineAsyncComponent(() => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')), ModalSettings: defineAsyncComponent(() => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings.vue')),
TheScratchpad: defineAsyncComponent(() => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad')), TheScratchpad: defineAsyncComponent(() => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad.vue')),
BaseTextEditor: defineAsyncComponent(() => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor')) BaseTextEditor: defineAsyncComponent(() => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor.vue'))
}, },
setup () { setup () {
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();
@@ -64,12 +64,20 @@ export default {
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { checkVersionUpdate } = applicationStore; const { checkVersionUpdate } = applicationStore;
const { changeApplicationTheme } = settingsStore;
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
changeApplicationTheme(applicationTheme.value);// Forces persistentStore to save on file and mail process
}, 1000);
});
return { return {
isLoading, isLoading,
isSettingModal, isSettingModal,
isScratchpad, isScratchpad,
checkVersionUpdate, checkVersionUpdate,
changeApplicationTheme,
connections, connections,
applicationTheme, applicationTheme,
disableBlur, disableBlur,
@@ -98,7 +106,7 @@ export default {
}, },
{ {
label: this.$t('message.selectAll'), label: this.$t('message.selectAll'),
role: 'selectall' role: 'selectAll'
} }
]); ]);
@@ -106,11 +114,12 @@ export default {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
let node = e.target; // eslint-disable-next-line @typescript-eslint/no-explicit-any
let node: any = e.target;
while (node) { while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) { if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup(getCurrentWindow()); InputMenu.popup({ window: getCurrentWindow() });
break; break;
} }
node = node.parentNode; node = node.parentNode;
@@ -147,7 +156,7 @@ export default {
height: calc(100vh - #{$footer-height}); height: calc(100vh - #{$footer-height});
} }
.connection-panel-wrapper{ .connection-panel-wrapper {
height: calc(100vh - #{$excluding-size}); height: calc(100vh - #{$excluding-size});
width: 100%; width: 100%;
padding-top: 15vh; padding-top: 15vh;
@@ -155,6 +164,6 @@ export default {
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
overflow: auto; overflow: auto;
} }
} }
</style> </style>

View File

@@ -3,7 +3,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active" :class="modalSizeClass"> <div class="modal active" :class="modalSizeClass">
<a class="modal-overlay" @click="hideModal" /> <a class="modal-overlay" @click="hideModal" />
<div class="modal-container"> <div ref="trapRef" class="modal-container">
<div v-if="hasHeader" class="modal-header pl-2"> <div v-if="hasHeader" class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<slot name="header" /> <slot name="header" />
@@ -46,65 +46,65 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { useFocusTrap } from '@/composables/useFocusTrap';
name: 'BaseConfirmModal', import { computed, onBeforeUnmount, PropType, useSlots } from 'vue';
props: {
size: {
type: String,
validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
default: 'small'
},
hideFooter: {
type: Boolean,
default: false
},
confirmText: String,
cancelText: String
},
emits: ['confirm', 'hide'],
computed: {
hasHeader () {
return !!this.$slots.header;
},
hasBody () {
return !!this.$slots.body;
},
hasDefault () {
return !!this.$slots.default;
},
modalSizeClass () {
if (this.size === 'small')
return 'modal-sm';
if (this.size === '400')
return 'modal-400';
else if (this.size === 'large')
return 'modal-lg';
else return '';
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
confirmModal () {
this.$emit('confirm');
this.hideModal();
},
hideModal () { const props = defineProps({
this.$emit('hide'); size: {
}, type: String as PropType<'small' | 'medium' | '400' | 'large'>,
onKey (e) { validator: (prop: string) => ['small', 'medium', '400', 'large'].includes(prop),
e.stopPropagation(); default: 'small'
if (e.key === 'Escape') },
this.hideModal(); hideFooter: {
} type: Boolean,
default: false
},
confirmText: String,
cancelText: String,
disableAutofocus: {
type: Boolean,
default: false
} }
});
const emit = defineEmits(['confirm', 'hide']);
const slots = useSlots();
const { trapRef } = useFocusTrap({ disableAutofocus: props.disableAutofocus });
const hasHeader = computed(() => !!slots.header);
const hasBody = computed(() => !!slots.body);
const hasDefault = computed(() => !!slots.default);
const modalSizeClass = computed(() => {
if (props.size === 'small')
return 'modal-sm';
if (props.size === '400')
return 'modal-400';
else if (props.size === 'large')
return 'modal-lg';
else return '';
});
const confirmModal = () => {
emit('confirm');
hideModal();
}; };
const hideModal = () => {
emit('hide');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
hideModal();
};
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -15,67 +15,60 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed, onBeforeUnmount, onMounted, Ref, ref } from 'vue';
name: 'BaseContextMenu',
props: {
contextEvent: MouseEvent
},
emits: ['close-context'],
data () {
return {
contextSize: null,
isBottom: false
};
},
computed: {
position () {
let topCord = 0;
let leftCord = 0;
if (this.contextEvent) { const contextContent: Ref<HTMLDivElement> = ref(null);
const { clientY, clientX } = this.contextEvent; const contextSize: Ref<DOMRect> = ref(null);
topCord = `${clientY + 2}px`; const isBottom: Ref<boolean> = ref(false);
leftCord = `${clientX + 5}px`; const props = defineProps<{contextEvent: MouseEvent}>();
const emit = defineEmits(['close-context']);
if (this.contextSize) { const position = computed(() => {
if (clientY + (this.contextSize.height < 200 ? 200 : this.contextSize.height) + 5 >= window.innerHeight) { let topCord = '0px';
topCord = `${clientY + 3 - this.contextSize.height}px`; let leftCord = '0px';
this.isBottom = true;
}
if (clientX + this.contextSize.width + 5 >= window.innerWidth) if (props.contextEvent) {
leftCord = `${clientX - this.contextSize.width}px`; const { clientY, clientX } = props.contextEvent;
} topCord = `${clientY + 2}px`;
leftCord = `${clientX + 5}px`;
if (contextSize.value) {
if (clientY + (contextSize.value.height < 200 ? 200 : contextSize.value.height) + 5 >= window.innerHeight) {
topCord = `${clientY + 3 - contextSize.value.height}px`;
isBottom.value = true;
} }
return { if (clientX + contextSize.value.width + 5 >= window.innerWidth)
top: topCord, leftCord = `${clientX - contextSize.value.width}px`;
left: leftCord
};
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.$refs.contextContent)
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
close () {
this.$emit('close-context');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.close();
} }
} }
return {
top: topCord,
left: leftCord
};
});
const close = () => {
emit('close-context');
}; };
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
close();
};
window.addEventListener('keydown', onKey);
onMounted(() => {
if (contextContent.value)
contextSize.value = contextContent.value.getBoundingClientRect();
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -4,11 +4,6 @@
</div> </div>
</template> </template>
<script>
export default {
name: 'BaseLoader'
};
</script>
<style scoped> <style scoped>
.empty { .empty {
position: absolute; position: absolute;

View File

@@ -1,95 +1,93 @@
<template> <template>
<div id="map" class="map" /> <div id="map" class="map" />
</template> </template>
<script>
import L from 'leaflet'; <script setup lang="ts">
import { onMounted, PropType, Ref, ref } from 'vue';
import * as L from 'leaflet';
import { import {
point, point,
lineString, lineString,
polygon polygon
} from '@turf/helpers'; } from '@turf/helpers';
import { GeoJsonObject } from 'geojson';
import { getArrayDepth } from 'common/libs/getArrayDepth'; import { getArrayDepth } from 'common/libs/getArrayDepth';
export default { interface Coordinates { x: number; y: number }
name: 'BaseMap',
props: {
points: [Object, Array],
isMultiSpatial: Boolean
},
data () {
return {
map: null,
markers: [],
center: null
};
},
mounted () {
if (this.isMultiSpatial) {
for (const element of this.points)
this.markers.push(this.getMarkers(element));
}
else {
this.markers = this.getMarkers(this.points);
if (!Array.isArray(this.points)) const props = defineProps({
this.center = [this.points.y, this.points.x]; points: [Object, Array] as PropType<Coordinates | Coordinates[]>,
} isMultiSpatial: Boolean
});
const map: Ref<L.Map> = ref(null);
const markers: Ref<GeoJsonObject | GeoJsonObject[]> = ref(null);
const center: Ref<[number, number]> = ref(null);
this.map = L.map('map', { const getMarkers = (points: Coordinates) => {
center: this.center || [0, 0], if (Array.isArray(points)) {
zoom: 15, if (getArrayDepth(points) === 1)
minZoom: 1, return lineString(points.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
attributionControl: false else
}); return polygon(points.map(arr => arr.reduce((acc: Coordinates[], curr: Coordinates) => [...acc, [curr.x, curr.y]], [])));
L.control.attribution({ prefix: '<b>Leaflet</b>' }).addTo(this.map);
const geoJsonObj = L.geoJSON(this.markers, {
style: function () {
return {
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
};
},
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {
radius: 7,
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
});
}
}).addTo(this.map);
const southWest = L.latLng(-90, -180);
const northEast = L.latLng(90, 180);
const bounds = L.latLngBounds(southWest, northEast);
this.map.setMaxBounds(bounds);
if (!this.center) this.map.fitBounds(geoJsonObj.getBounds());
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <b>OpenStreetMap</b>'
}).addTo(this.map);
},
methods: {
getMarkers (points) {
if (Array.isArray(points)) {
if (getArrayDepth(points) === 1)
return lineString(points.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(points.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([points.x, points.y]);
}
} }
else
return point([points.x, points.y]);
}; };
onMounted(() => {
if (props.isMultiSpatial) {
for (const element of props.points as Coordinates[])
(markers.value as GeoJsonObject[]).push(getMarkers(element));
}
else {
markers.value = getMarkers(props.points as Coordinates);
if (!Array.isArray(props.points))
center.value = [props.points.y, props.points.x];
}
map.value = L.map('map', {
center: center.value || [0, 0],
zoom: 15,
minZoom: 1,
attributionControl: false
});
L.control.attribution({ prefix: '<b>Leaflet</b>' }).addTo(map.value);
const geoJsonObj = L.geoJSON((markers.value as GeoJsonObject), {
style: function () {
return {
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
};
},
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {
radius: 7,
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
});
}
}).addTo(map.value);
const southWest = L.latLng(-90, -180);
const northEast = L.latLng(90, 180);
const bounds = L.latLngBounds(southWest, northEast);
map.value.setMaxBounds(bounds);
if (!center.value) map.value.fitBounds(geoJsonObj.getBounds());
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <b>OpenStreetMap</b>'
}).addTo(map.value);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -14,64 +14,58 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed, ref } from 'vue';
name: 'BaseNotification',
props: {
message: {
type: String,
default: ''
},
status: {
type: String,
default: ''
}
},
emits: ['close'],
data () {
return {
isExpanded: false
};
},
computed: {
notificationStatus () {
let className = '';
let iconName = '';
switch (this.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName }; const props = defineProps({
}, message: {
isExpandable () { type: String,
return this.message.length > 80; default: ''
}
}, },
methods: { status: {
hideToast () { type: String,
this.$emit('close'); default: ''
},
toggleExpand () {
this.isExpanded = !this.isExpanded;
}
} }
});
const isExpanded = ref(false);
const emit = defineEmits(['close']);
const notificationStatus = computed(() => {
let className = '';
let iconName = '';
switch (props.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName };
});
const isExpandable = computed(() => props.message.length > 80);
const hideToast = () => {
emit('close');
};
const toggleExpand = () => {
isExpanded.value = !isExpanded.value;
}; };
</script> </script>
<style scoped> <style scoped>
.toast { .toast {
display: flex; display: flex;

View File

@@ -0,0 +1,462 @@
<template>
<div
ref="el"
class="select"
:class="{'select--open': isOpen, 'select--disabled': disabled}"
role="combobox"
:tabindex="searchable || disabled ? -1 : tabindex"
@focus="activate()"
@blur="searchable ? false : handleBlurEvent()"
@keyup.esc="deactivate()"
@keydown.self.down.prevent="moveDown()"
@keydown.self.up.prevent="moveUp"
>
<div class="select__item-text">
<input
v-if="searchable"
ref="searchInput"
class="select__search-input"
:style="searchInputStyle"
type="text"
autocomplete="off"
spellcheck="false"
:tabindex="tabindex"
:value="searchText"
@input="searchText = $event.target.value"
@focus.prevent="!isOpen ? activate() : false"
@blur.prevent="handleBlurEvent()"
@keyup.esc="deactivate()"
@keydown.down.prevent="keyArrows('down')"
@keydown.up.prevent="keyArrows('up')"
@keypress.enter.prevent.stop.self="select(filteredOptions[hightlightedIndex])"
>
<span v-if="searchable && !isOpen || !searchable">{{ currentOptionLabel }}</span>
</div>
<Transition :name="animation">
<div
v-if="isOpen"
ref="optionList"
:class="`select__list-wrapper ${dropdownClass ? dropdownClass : '' }`"
@mousedown="isMouseDown = true"
@mouseup="handleMouseUpEvent()"
>
<ul class="select__list" @mousedown.prevent>
<li
v-for="(opt, index) of filteredOptions"
:key="opt.id"
:ref="(el) => optionRefs[index] = el"
:class="{
'select__item': true,
'select__group': opt.$type === 'group',
'select__option--highlight': opt.$type === 'option' && !opt.disabled && index === hightlightedIndex,
'select__option--selected': opt.$type === 'option' && isSelected(opt),
'select__option--disabled': opt.disabled
}"
@click.stop="select(opt)"
@mousemove.self="hightlightedIndex = index"
>
<slot
name="option"
:option="opt"
:index="index"
>
{{ opt.label }}
</slot>
</li>
</ul>
</div>
</Transition>
</div>
</template>
<script>
import { defineComponent, computed, ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
export default defineComponent({
name: 'BaseSelect',
props: {
modelValue: {
type: [String, Number, Object, Boolean]
},
value: {
type: [String, Number, Object, Boolean]
},
searchable: {
type: Boolean,
default: true
},
preserveSearch: {
type: Boolean,
default: false
},
tabindex: {
type: Number,
default: 0
},
options: {
type: Array,
default: () => []
},
optionTrackBy: {
type: [String, Function],
default: 'value'
},
optionLabel: {
type: [String, Function],
default: 'label'
},
optionDisabled: {
type: Function
},
groupLabel: {
type: String
},
groupValues: {
type: String
},
closeOnSelect: {
type: Boolean,
default: true
},
animation: {
type: String,
default: 'fade-slide-down'
},
dropdownOffsets: {
type: Object,
default: () => ({ top: 10, left: 0 })
},
dropdownClass: {
type: String
},
disabled: {
type: Boolean,
default: false
},
maxVisibleOptions: {
type: Number,
default: 100
}
},
emits: ['select', 'open', 'close', 'update:modelValue', 'change', 'blur'],
setup (props, { emit }) {
const hightlightedIndex = ref(0);
const isOpen = ref(false);
const isMouseDown = ref(false);
const internalValue = ref(props.modelValue !== false ? props.modelValue : props.value);
const el = ref(null);
const searchInput = ref(null);
const optionList = ref(null);
const optionRefs = [];
const searchText = ref('');
const getOptionValue = (opt) => _guess('optionTrackBy', opt);
const getOptionLabel = (opt) => _guess('optionLabel', opt) + '';
const getOptionDisabled = (opt) => _guess('optionDisabled', opt);
const _guess = (name, item) => {
const prop = props[name];
if (typeof prop === 'function')
return prop(item);
return item[prop] !== undefined ? item[prop] : item;
};
const flattenOptions = computed(() => {
return [...props.options].reduce((prev, curr) => {
if (curr[props.groupValues] && curr[props.groupValues].length) {
prev.push({
$type: 'group',
label: curr[props.groupLabel],
id: `group-${curr[props.groupLabel]}`,
count: curr[props.groupLabel].length
});
return prev.concat(curr[props.groupValues].map(el => {
const value = getOptionValue(el);
return {
$type: 'option',
label: getOptionLabel(el),
id: `option-${value}`,
disabled: getOptionDisabled(el) === true,
value,
$data: {
...el
}
};
}));
}
else {
const value = getOptionValue(curr);
prev.push({
$type: 'option',
label: getOptionLabel(curr),
id: `option-${value}`,
disabled: getOptionDisabled(curr) === true,
value,
$data: {
...curr
}
});
}
return prev;
}, []);
});
const filteredOptions = computed(() => {
const searchTerms = (searchText.value || '').toLowerCase().trim();
let options = searchTerms
? flattenOptions.value.filter(opt => opt.$type === 'group' || opt.label.trim().toLowerCase().indexOf(searchTerms) !== -1)
: flattenOptions.value;
if (options.length > props.maxVisibleOptions) {
let sliceStart = 0;
let sliceEnd = sliceStart + props.maxVisibleOptions;
// if no search active try to open the dropdown showing options around the selected one
if (searchTerms === '') {
const index = internalValue.value ? flattenOptions.value.findIndex(el => el.value === internalValue.value) : -1;
if (index < options.length -1) {
sliceStart = Math.max(0, index - Math.floor(props.maxVisibleOptions / 2));
sliceEnd = Math.min(sliceStart + sliceEnd, options.length -1);
}
}
options = options.slice(sliceStart, sliceEnd);
}
return options;
});
const searchInputStyle = computed(() => {
if (props.searchable)
// just hide the input and give the ability to receive focus
return isOpen.value ? { with: '100%' } : { width: 0, position: 'absolute', padding: 0, margin: 0 };
return '';
});
watch(filteredOptions, (options) => {
if (hightlightedIndex.value >= options.length -1)
hightlightedIndex.value = options.length ? options.length -1 : 0;
else
hightlightedIndex.value = 0;
});
watch(() => props.modelValue, (val) => {
internalValue.value = val;
});
watch(() => props.value, (val) => {
internalValue.value = val;
});
const currentOptionLabel = computed(() =>
flattenOptions.value.find(d => d.value === internalValue.value)?.label
);
const select = (opt) => {
if (opt.$type === 'group' || opt.disabled) return;
internalValue.value = opt.value;
emit('select', opt);
emit('update:modelValue', opt.value);
emit('change', opt);
if (props.closeOnSelect)
deactivate();
};
const isSelected = (opt) => {
return internalValue.value === opt.value;
};
const activate = () => {
if (isOpen.value || props.disabled) return;
isOpen.value = true;
hightlightedIndex.value = filteredOptions.value.findIndex(el => el.value === internalValue.value) || 0;
if (props.searchable)
searchInput.value.focus();
else
el.value.focus();
nextTick(() => {
adjustListPosition();
scrollTo(optionRefs[hightlightedIndex.value]);
});
emit('open');
};
const deactivate = () => {
if (!isOpen.value) return;
isOpen.value = false;
if (props.searchable)
searchInput.value?.blur();
else
el.value?.blur();
if (!props.preserveSearch) searchText.value = '';
emit('close');
};
const adjustListPosition = () => {
if (!optionList.value) return;
const element = el.value;
let { left, top } = element.getBoundingClientRect();
const { left: offsetLeft = 0, top: offsetTop = 0 } = props.dropdownOffsets;
top = top + element.clientHeight + offsetTop;
const openBottom = top >= 0 && top + optionList.value.clientHeight <= window.innerHeight;
if (!openBottom) {
top -= (offsetTop * 2 + element.clientHeight);
optionList.value.style.transform = 'translate(0, -100%)';
}
optionList.value.style.left = `${left + offsetLeft}px`;
optionList.value.style.top = `${top}px`;
optionList.value.style.minWidth = `${element.clientWidth}px`;
};
const keyArrows = (direction) => {
const sum = direction === 'down' ? +1 : -1;
let index = hightlightedIndex.value + sum;
index = Math.max(0, index > filteredOptions.value.length - 1 ? filteredOptions.value.length - 1 : index);
if (filteredOptions.value[index].$type === 'group')
index=Math.max(1, index+sum);
hightlightedIndex.value = index;
const optEl = optionRefs[hightlightedIndex.value];
if (!optEl)
return;
scrollTo(optEl);
};
const scrollTo = (optEl) => {
if (!optEl) return;
const visMin = optionList.value.scrollTop;
const visMax = optionList.value.scrollTop + optionList.value.clientHeight - optEl.clientHeight;
if (optEl.offsetTop < visMin)
optionList.value.scrollTop = optEl.offsetTop;
else if (optEl.offsetTop >= visMax)
optionList.value.scrollTop = optEl.offsetTop - optionList.value.clientHeight + optEl.clientHeight;
};
const handleBlurEvent = () => {
if (isMouseDown.value) return;
deactivate();
emit('blur');
};
const handleMouseUpEvent = () => {
isMouseDown.value = false;
searchInput.value.focus();
};
const handleWheelEvent = (e) => {
if (!e.target.className.includes('select__')) deactivate();
};
onMounted(() => {
window.addEventListener('resize', adjustListPosition);
window.addEventListener('wheel', handleWheelEvent);
nextTick(() => {
// fix position when the component is created and opened at the same time
if (isOpen.value) {
setTimeout(() => {
adjustListPosition();
}, 50);
}
});
});
onUnmounted(() => {
window.removeEventListener('resize', adjustListPosition);
window.removeEventListener('wheel', handleWheelEvent);
});
return {
el,
searchInput,
searchText,
searchInputStyle,
filteredOptions,
currentOptionLabel,
activate,
deactivate,
select,
isSelected,
keyArrows,
isOpen,
isMouseDown,
hightlightedIndex,
optionList,
optionRefs,
handleBlurEvent,
handleMouseUpEvent,
internalValue
};
}
});
</script>
<style lang="scss" scoped>
.select {
display: block;
&:focus,
&--open {
z-index: 10;
}
&__search-input {
appearance: none;
border: none;
background: transparent;
outline: none;
color: currentColor;
max-width: 100%;
width: 100%;
}
&__list-wrapper {
cursor: pointer;
position: fixed;
display: block;
z-index: 5;
-webkit-overflow-scrolling: touch;
max-height: 240px;
overflow: auto;
left: 0;
top: 40px;
}
&__list {
list-style: none;
}
&__option {
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
</style>

View File

@@ -9,121 +9,111 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { onMounted, watch } from 'vue';
import * as ace from 'ace-builds'; import * as ace from 'ace-builds';
import { storeToRefs } from 'pinia';
import 'ace-builds/webpack-resolver'; import 'ace-builds/webpack-resolver';
import { storeToRefs } from 'pinia';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
export default { const props = defineProps({
name: 'BaseTextEditor', modelValue: String,
props: { mode: { type: String, default: 'text' },
modelValue: String, editorClass: { type: String, default: '' },
mode: { type: String, default: 'text' }, autoFocus: { type: Boolean, default: false },
editorClass: { type: String, default: '' }, readOnly: { type: Boolean, default: false },
autoFocus: { type: Boolean, default: false }, showLineNumbers: { type: Boolean, default: true },
readOnly: { type: Boolean, default: false }, height: { type: Number, default: 200 }
showLineNumbers: { type: Boolean, default: true }, });
height: { type: Number, default: 200 } const emit = defineEmits(['update:modelValue']);
}, const settingsStore = useSettingsStore();
emits: ['update:modelValue'],
setup () {
const settingsStore = useSettingsStore();
const { const {
editorTheme, editorTheme,
editorFontSize, editorFontSize,
autoComplete, autoComplete,
lineWrap lineWrap
} = storeToRefs(settingsStore); } = storeToRefs(settingsStore);
return { let editor: ace.Ace.Editor;
editorTheme, const id = uidGen();
editorFontSize,
autoComplete,
lineWrap
};
},
data () {
return {
editor: null,
id: uidGen()
};
},
watch: {
mode () {
if (this.editor)
this.editor.session.setMode(`ace/mode/${this.mode}`);
},
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
editorFontSize () {
const sizes = {
small: '12px',
medium: '14px',
large: '16px'
};
if (this.editor) { watch(() => props.mode, () => {
this.editor.setOptions({ if (editor)
fontSize: sizes[this.editorFontSize] editor.session.setMode(`ace/mode/${props.mode}`);
}); });
}
}, watch(editorTheme, () => {
autoComplete () { if (editor)
if (this.editor) { editor.setTheme(`ace/theme/${editorTheme.value}`);
this.editor.setOptions({ });
enableLiveAutocompletion: this.autoComplete
}); watch(editorFontSize, () => {
} const sizes = {
}, small: 12,
lineWrap () { medium: 14,
if (this.editor) { large: 16
this.editor.setOptions({ };
wrap: this.lineWrap
}); if (editor) {
} editor.setOptions({
} fontSize: sizes[editorFontSize.value as undefined as 'small' | 'medium' | 'large']
},
mounted () {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.modelValue || '',
fontSize: '14px',
printMargin: false,
readOnly: this.readOnly,
showLineNumbers: this.showLineNumbers,
showGutter: this.showLineNumbers
}); });
}
});
this.editor.setOptions({ watch(autoComplete, () => {
enableBasicAutocompletion: false, if (editor) {
wrap: this.lineWrap, editor.setOptions({
enableSnippets: false, enableLiveAutocompletion: autoComplete.value
enableLiveAutocompletion: false
}); });
}
});
this.editor.session.on('change', () => { watch(lineWrap, () => {
const content = this.editor.getValue(); if (editor) {
this.$emit('update:modelValue', content); editor.setOptions({
wrap: lineWrap.value
}); });
}
});
if (this.autoFocus) { onMounted(() => {
setTimeout(() => { editor = ace.edit(`editor-${id}`, {
this.editor.focus(); mode: `ace/mode/${props.mode}`,
this.editor.resize(); theme: `ace/theme/${editorTheme.value}`,
}, 20); value: props.modelValue || '',
} fontSize: 14,
printMargin: false,
readOnly: props.readOnly,
showLineNumbers: props.showLineNumbers,
showGutter: props.showLineNumbers
});
editor.setOptions({
enableBasicAutocompletion: false,
wrap: lineWrap,
enableSnippets: false,
enableLiveAutocompletion: false
});
(editor.session as unknown as ace.Ace.Editor).on('change', () => {
const content = editor.getValue();
emit('update:modelValue', content);
});
if (props.autoFocus) {
setTimeout(() => { setTimeout(() => {
this.editor.resize(); editor.focus();
editor.resize();
}, 20); }, 20);
} }
};
setTimeout(() => {
editor.resize();
}, 20);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -9,67 +9,63 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed, ref, watch } from 'vue';
name: 'BaseToast',
props: {
message: {
type: String,
default: ''
},
status: {
type: String,
default: ''
}
},
emits: ['close'],
data () {
return {
isVisible: false
};
},
computed: {
toastStatus () {
let className = '';
let iconName = '';
switch (this.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName }; const props = defineProps({
} message: {
type: String,
default: ''
}, },
watch: { status: {
message: function () { type: String,
if (this.message) default: ''
this.isVisible = true;
else
this.isVisible = false;
}
},
methods: {
hideToast () {
this.isVisible = false;
this.$emit('close');
}
} }
});
const isVisible = ref(false);
const message = ref(props.message);
const emit = defineEmits(['close']);
const toastStatus = computed(() => {
let className = '';
let iconName = '';
switch (props.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName };
});
watch(message, () => {
if (message.value)
isVisible.value = true;
else
isVisible.value = false;
});
const hideToast = () => {
isVisible.value = false;
emit('close');
}; };
</script> </script>
<style scoped> <style scoped>
.toast { .toast {
display: flex; display: flex;

View File

@@ -7,7 +7,7 @@
{{ lastPart(modelValue) }} {{ lastPart(modelValue) }}
</span> </span>
<i <i
v-if="modelValue.length" v-if="modelValue"
class="file-uploader-reset mdi mdi-close" class="file-uploader-reset mdi mdi-close"
@click.prevent="clear" @click.prevent="clear"
/> />
@@ -22,41 +22,35 @@
</label> </label>
</template> </template>
<script> <script setup lang="ts">
export default { import { uidGen } from 'common/libs/uidGen';
name: 'BaseUploadInput',
props: {
message: {
default: 'Browse',
type: String
},
modelValue: {
default: '',
type: String
}
},
emits: ['change', 'clear'],
data () {
return {
id: null
};
},
mounted () {
this.id = this._uid;
},
methods: {
clear () {
this.$emit('clear');
},
lastPart (string) {
if (!string) return '';
string = string.split(/[/\\]+/).pop(); defineProps({
if (string.length >= 19) message: {
string = `...${string.slice(-19)}`; default: 'Browse',
return string; type: String
} },
modelValue: {
default: '',
type: String
} }
});
const emit = defineEmits(['change', 'clear']);
const id = uidGen();
const clear = () => {
emit('clear');
};
const lastPart = (string: string) => {
if (!string) return '';
string = string.split(/[/\\]+/).pop();
if (string.length >= 19)
string = `...${string.slice(-19)}`;
return string;
}; };
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="vscroll-holder"> <div ref="root" class="vscroll-holder">
<div <div
class="vscroll-spacer" class="vscroll-spacer"
:style="{ :style="{
@@ -20,71 +20,76 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { onBeforeUnmount, onMounted, Ref, ref, watch } from 'vue';
name: 'BaseVirtualScroll',
props: {
items: Array,
itemHeight: Number,
visibleHeight: Number,
scrollElement: {
type: HTMLDivElement,
default: null
}
},
data () {
return {
topHeight: 0,
bottomHeight: 0,
visibleItems: [],
renderTimeout: null,
localScrollElement: null
};
},
watch: {
scrollElement () {
this.setScrollElement();
}
},
mounted () {
this.setScrollElement();
},
beforeUnmount () {
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
},
methods: {
checkScrollPosition (e) {
clearTimeout(this.renderTimeout);
this.renderTimeout = setTimeout(() => { const props = defineProps({
this.updateWindow(e); items: Array,
}, 200); itemHeight: Number,
}, visibleHeight: Number,
updateWindow () { scrollElement: {
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight); type: HTMLDivElement,
const totalScrollHeight = this.items.length * this.itemHeight; default: null
const offset = 50;
const scrollTop = this.localScrollElement.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);
const lastCutIndex = lastVisibleIndex + offset;
this.visibleItems = this.items.slice(firstCutIndex, lastCutIndex);
this.topHeight = firstCutIndex * this.itemHeight;
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
},
setScrollElement () {
if (this.localScrollElement)
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this.checkScrollPosition);
}
} }
});
const root = ref(null);
const topHeight: Ref<number> = ref(0);
const bottomHeight: Ref<number> = ref(0);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const visibleItems: Ref<any[]> = ref([]);
const renderTimeout: Ref<NodeJS.Timeout> = ref(null);
const localScrollElement: Ref<HTMLDivElement> = ref(null);
const checkScrollPosition = () => {
clearTimeout(renderTimeout.value);
renderTimeout.value = setTimeout(() => {
updateWindow();
}, 200);
}; };
const updateWindow = () => {
const visibleItemsCount = Math.ceil(props.visibleHeight / props.itemHeight);
const totalScrollHeight = props.items.length * props.itemHeight;
const offset = 50;
const scrollTop = localScrollElement.value.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);
const lastCutIndex = lastVisibleIndex + offset;
visibleItems.value = props.items.slice(firstCutIndex, lastCutIndex);
topHeight.value = firstCutIndex * props.itemHeight;
bottomHeight.value = totalScrollHeight - visibleItems.value.length * props.itemHeight - topHeight.value;
};
const setScrollElement = () => {
if (localScrollElement.value)
localScrollElement.value.removeEventListener('scroll', checkScrollPosition);
localScrollElement.value = props.scrollElement ? props.scrollElement : root.value;
updateWindow();
localScrollElement.value.addEventListener('scroll', checkScrollPosition);
};
watch(() => props.scrollElement, () => {
setScrollElement();
});
onMounted(() => {
setScrollElement();
});
onBeforeUnmount(() => {
localScrollElement.value.removeEventListener('scroll', checkScrollPosition);
});
defineExpose({
updateWindow
});
</script> </script>

View File

@@ -1,38 +1,26 @@
<template> <template>
<fieldset class="input-group mb-0"> <fieldset class="input-group mb-0">
<select <BaseSelect
v-model="selectedGroup" v-model="selectedGroup"
class="form-select" class="form-select"
:options="[{name: 'manual'}, ...fakerGroups]"
:option-label="(opt: any) => opt.name === 'manual' ? $t('message.manualValue') : $t(`faker.${opt.name}`)"
option-track-by="name"
:disabled="!isChecked" :disabled="!isChecked"
style="flex-grow: 0;" style="flex-grow: 0;"
@change="onChange" @change="onChange"
> />
<option value="manual">
{{ $t('message.manualValue') }} <BaseSelect
</option>
<option
v-for="group in fakerGroups"
:key="group.name"
:value="group.name"
>
{{ $t(`faker.${group.name}`) }}
</option>
</select>
<select
v-if="selectedGroup !== 'manual'" v-if="selectedGroup !== 'manual'"
v-model="selectedMethod" v-model="selectedMethod"
:options="fakerMethods"
:option-label="(opt: any) => $t(`faker.${opt.name}`)"
option-track-by="name"
class="form-select" class="form-select"
:disabled="!isChecked" :disabled="!isChecked"
@change="onChange" @change="onChange"
> />
<option
v-for="method in fakerMethods"
:key="method.name"
:value="method.name"
>
{{ $t(`faker.${method.name}`) }}
</option>
</select>
<ForeignKeySelect <ForeignKeySelect
v-else-if="foreignKeys.includes(field.name)" v-else-if="foreignKeys.includes(field.name)"
ref="formInput" ref="formInput"
@@ -52,7 +40,7 @@
> >
<BaseUploadInput <BaseUploadInput
v-else-if="inputProps().type === 'file'" v-else-if="inputProps().type === 'file'"
:value="selectedValue" :model-value="selectedValue"
:message="$t('word.browse')" :message="$t('word.browse')"
@clear="clearValue" @clear="clearValue"
@change="filesChange($event)" @change="filesChange($event)"
@@ -66,21 +54,14 @@
:type="inputProps().type" :type="inputProps().type"
:disabled="!isChecked" :disabled="!isChecked"
> >
<select <BaseSelect
v-else-if="enumArray" v-else-if="enumArray"
v-model="selectedValue" v-model="selectedValue"
:options="enumArray"
class="form-select" class="form-select"
:disabled="!isChecked" :disabled="!isChecked"
@change="onChange" @change="onChange"
> />
<option
v-for="val in enumArray"
:key="val"
:value="val"
>
{{ val }}
</option>
</select>
<input <input
v-else v-else
ref="formInput" ref="formInput"
@@ -104,151 +85,149 @@
</fieldset> </fieldset>
</template> </template>
<script> <script setup lang="ts">
import { computed, PropType, Ref, ref, watch } from 'vue';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput.vue';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect.vue';
import FakerMethods from 'common/FakerMethods'; import FakerMethods from 'common/FakerMethods';
import BaseSelect from '@/components/BaseSelect.vue';
export default { const props = defineProps({
name: 'FakerSelect', type: String,
components: { field: Object,
ForeignKeySelect, isChecked: Boolean,
BaseUploadInput foreignKeys: Array,
}, keyUsage: Array as PropType<{field: string}[]>,
props: { fieldLength: Number,
type: String, fieldObj: Object
field: Object, });
isChecked: Boolean, const emit = defineEmits(['update:modelValue']);
foreignKeys: Array,
keyUsage: Array,
fieldLength: Number,
fieldObj: Object
},
emits: ['update:modelValue'],
data () {
return {
localType: null,
selectedGroup: 'manual',
selectedMethod: '',
selectedValue: '',
debounceTimeout: null,
methodParams: {},
enumArray: null
};
},
computed: {
fakerGroups () {
if ([...TEXT, ...LONG_TEXT].includes(this.type))
this.localType = 'string';
else if (NUMBER.includes(this.type))
this.localType = 'number';
else if (FLOAT.includes(this.type))
this.localType = 'float';
else if ([...DATE, ...DATETIME].includes(this.type))
this.localType = 'datetime';
else if (TIME.includes(this.type))
this.localType = 'time';
else
this.localType = 'none';
return FakerMethods.getGroupsByType(this.localType); const localType: Ref<string> = ref(null);
}, const selectedGroup: Ref<string> = ref('manual');
fakerMethods () { const selectedMethod: Ref<string> = ref('');
return FakerMethods.getMethods({ type: this.localType, group: this.selectedGroup }); const selectedValue: Ref<string> = ref('');
}, const debounceTimeout: Ref<NodeJS.Timeout> = ref(null);
methodData () { const methodParams: Ref<{[key: string]: string}> = ref({});
return this.fakerMethods.find(method => method.name === this.selectedMethod); const enumArray: Ref<string[]> = ref(null);
}
},
watch: {
fieldObj () {
if (this.fieldObj) {
if (Array.isArray(this.fieldObj.value)) {
this.enumArray = this.fieldObj.value;
this.selectedValue = this.fieldObj.value[0];
}
else
this.selectedValue = this.fieldObj.value;
}
},
selectedGroup () {
if (this.fakerMethods.length)
this.selectedMethod = this.fakerMethods[0].name;
else
this.selectedMethod = '';
},
selectedMethod () {
this.onChange();
},
selectedValue () {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = null;
this.debounceTimeout = setTimeout(() => {
this.onChange();
}, 200);
}
},
methods: {
inputProps () {
if ([...TEXT, ...LONG_TEXT].includes(this.type))
return { type: 'text', mask: false };
if ([...NUMBER, ...FLOAT].includes(this.type)) const fakerGroups = computed(() => {
return { type: 'number', mask: false }; if ([...TEXT, ...LONG_TEXT].includes(props.type))
localType.value = 'string';
else if (NUMBER.includes(props.type))
localType.value = 'number';
else if (FLOAT.includes(props.type))
localType.value = 'float';
else if ([...DATE, ...DATETIME].includes(props.type))
localType.value = 'datetime';
else if (TIME.includes(props.type))
localType.value = 'time';
else
localType.value = 'none';
if (TIME.includes(this.type)) { return FakerMethods.getGroupsByType(localType.value);
let timeMask = '##:##:##'; });
const precision = this.fieldLength;
for (let i = 0; i < precision; i++) const fakerMethods = computed(() => {
timeMask += i === 0 ? '.#' : '#'; return FakerMethods.getMethods({ type: localType.value, group: selectedGroup.value });
});
return { type: 'text', mask: timeMask }; const methodData = computed(() => {
} return fakerMethods.value.find(method => method.name === selectedMethod.value);
});
if (DATE.includes(this.type)) const inputProps = () => {
return { type: 'text', mask: '####-##-##' }; if ([...TEXT, ...LONG_TEXT].includes(props.type))
return { type: 'text', mask: false };
if (DATETIME.includes(this.type)) { if ([...NUMBER, ...FLOAT].includes(props.type))
let datetimeMask = '####-##-## ##:##:##'; return { type: 'number', mask: false };
const precision = this.fieldLength;
for (let i = 0; i < precision; i++) if (TIME.includes(props.type)) {
datetimeMask += i === 0 ? '.#' : '#'; let timeMask = '##:##:##';
const precision = props.fieldLength;
return { type: 'text', mask: datetimeMask }; for (let i = 0; i < precision; i++)
} timeMask += i === 0 ? '.#' : '#';
if (BLOB.includes(this.type)) return { type: 'text', mask: timeMask };
return { type: 'file', mask: false };
if (BIT.includes(this.type))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.field === keyName);
},
filesChange (event) {
const { files } = event.target;
if (!files.length) return;
this.selectedValue = files[0].path;
},
clearValue () {
this.selectedValue = '';
},
onChange () {
this.$emit('update:modelValue', {
group: this.selectedGroup,
method: this.selectedMethod,
params: this.methodParams,
value: this.selectedValue,
length: this.fieldLength
});
}
} }
if (DATE.includes(props.type))
return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(props.type)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = props.fieldLength;
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
if (BLOB.includes(props.type))
return { type: 'file', mask: false };
if (BIT.includes(props.type))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
}; };
const getKeyUsage = (keyName: string) => {
return props.keyUsage.find(key => key.field === keyName);
};
const filesChange = ({ target } : {target: HTMLInputElement }) => {
const { files } = target;
if (!files.length) return;
selectedValue.value = files[0].path;
};
const clearValue = () => {
selectedValue.value = '';
};
const onChange = () => {
emit('update:modelValue', {
group: selectedGroup.value,
method: selectedMethod.value,
params: methodParams.value,
value: selectedValue.value,
length: props.fieldLength
});
};
watch(() => props.fieldObj, () => {
if (props.fieldObj) {
if (Array.isArray(props.fieldObj.value)) {
enumArray.value = props.fieldObj.value;
selectedValue.value = props.fieldObj.value[0];
}
else
selectedValue.value = props.fieldObj.value;
}
});
watch(selectedGroup, () => {
if (fakerMethods.value.length)
selectedMethod.value = fakerMethods.value[0].name;
else
selectedMethod.value = '';
});
watch(selectedMethod, () => {
onChange();
});
watch(selectedValue, () => {
clearTimeout(debounceTimeout.value);
debounceTimeout.value = null;
debounceTimeout.value = setTimeout(() => {
onChange();
}, 200);
});
</script> </script>

View File

@@ -1,107 +1,107 @@
<template> <template>
<select <BaseSelect
ref="editField" ref="editField"
:options="foreigns"
class="form-select pl-1 pr-4" class="form-select pl-1 pr-4"
:class="{'small-select': size === 'small'}" :class="{'small-select': size === 'small'}"
:value="currentValue"
dropdown-class="select-sm"
dropdown-container=".workspace-query-results > .vscroll"
@change="onChange" @change="onChange"
@blur="$emit('blur')" @blur="emit('blur')"
> />
<option v-if="!isValidDefault" :value="modelValue">
{{ modelValue === null ? 'NULL' : modelValue }}
</option>
<option
v-for="row in foreignList"
:key="row.foreign_column"
:value="row.foreign_column"
:selected="row.foreign_column === modelValue"
>
{{ row.foreign_column }} {{ cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '') }}
</option>
</select>
</template> </template>
<script> <script setup lang="ts">
import { computed, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { TEXT, LONG_TEXT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT } from 'common/fieldTypes';
export default { import BaseSelect from '@/components/BaseSelect.vue';
name: 'ForeignKeySelect', import { TableField } from 'common/interfaces/antares';
props: {
modelValue: [String, Number],
keyUsage: Object,
size: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'blur'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
modelValue: [String, Number],
return { addNotification, selectedWorkspace }; keyUsage: Object,
}, size: {
data () { type: String,
return { default: ''
foreignList: []
};
},
computed: {
isValidDefault () {
if (!this.foreignList.length) return true;
if (this.modelValue === null) return false;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.modelValue.toString());
}
},
async created () {
let foreignDesc;
const params = {
uid: this.selectedWorkspace,
schema: this.keyUsage.refSchema,
table: this.keyUsage.refTable
};
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type) && field.name !== this.keyUsage.refField);
foreignDesc = textField ? textField.name : false;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
try { // Foregn list
const { status, response } = await Tables.getForeignList({
...params,
column: this.keyUsage.refField,
description: foreignDesc
});
if (status === 'success')
this.foreignList = response.rows;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
methods: {
onChange () {
this.$emit('update:modelValue', this.$refs.editField.value);
},
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
}
} }
});
const emit = defineEmits(['update:modelValue', 'blur']);
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const editField: Ref<HTMLSelectElement> = ref(null);
const foreignList = ref([]);
const currentValue = ref(props.modelValue);
const isValidDefault = computed(() => {
if (!foreignList.value.length) return true;
if (props.modelValue === null) return false;
return foreignList.value.some(foreign => foreign.foreign_column.toString() === props.modelValue.toString());
});
const foreigns = computed(() => {
const list = [];
if (!isValidDefault.value)
list.push({ value: props.modelValue, label: props.modelValue === null ? 'NULL' : props.modelValue });
for (const row of foreignList.value)
list.push({ value: row.foreign_column, label: `${row.foreign_column} ${cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '')}` });
return list;
});
const onChange = (opt: HTMLSelectElement) => {
emit('update:modelValue', opt.value);
}; };
const cutText = (val: string) => {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
};
let foreignDesc: string | false;
const params = {
uid: selectedWorkspace.value,
schema: props.keyUsage.refSchema,
table: props.keyUsage.refTable
};
(async () => {
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
const textField = (response as TableField[]).find((field: {type: string; name: string}) => [...TEXT, ...LONG_TEXT].includes(field.type) && field.name !== props.keyUsage.refField);
foreignDesc = textField ? textField.name : false;
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
try { // Foregn list
const { status, response } = await Tables.getForeignList({
...params,
column: props.keyUsage.refField,
description: foreignDesc
});
if (status === 'success')
foreignList.value = response.rows;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
})();
</script> </script>

View File

@@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active modal-sm"> <div class="modal active modal-sm">
<a class="modal-overlay" /> <a class="modal-overlay" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@@ -55,30 +55,28 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
export default { import { Ref, ref } from 'vue';
name: 'ModalAskCredentials', import { useFocusTrap } from '@/composables/useFocusTrap';
emits: ['close-asking', 'credentials'],
data () { const { trapRef } = useFocusTrap();
return {
credentials: { const credentials = ref({
user: '', user: '',
password: '' password: ''
} });
}; const firstInput: Ref<HTMLInputElement> = ref(null);
}, const emit = defineEmits(['close-asking', 'credentials']);
created () {
setTimeout(() => { const closeModal = () => {
this.$refs.firstInput.focus(); emit('close-asking');
}, 20);
},
methods: {
closeModal () {
this.$emit('close-asking');
},
sendCredentials () {
this.$emit('credentials', this.credentials);
}
}
}; };
const sendCredentials = () => {
emit('credentials', credentials.value);
};
setTimeout(() => {
firstInput.value.focus();
}, 20);
</script> </script>

View File

@@ -47,83 +47,75 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { computed, PropType, Ref, ref } from 'vue';
import { NUMBER, FLOAT } from 'common/fieldTypes'; import { NUMBER, FLOAT } from 'common/fieldTypes';
import ConfirmModal from '@/components/BaseConfirmModal'; import { FunctionInfos, RoutineInfos } from 'common/interfaces/antares';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
export default { const props = defineProps({
name: 'ModalAskParameters', localRoutine: Object as PropType<RoutineInfos | FunctionInfos>,
components: { client: String
ConfirmModal });
},
props: {
localRoutine: Object,
client: String
},
emits: ['confirm', 'close'],
data () {
return {
values: {}
};
},
computed: {
inParameters () {
return this.localRoutine.parameters.filter(param => param.context === 'IN');
}
},
created () {
window.addEventListener('keydown', this.onKey);
setTimeout(() => { const emit = defineEmits(['confirm', 'close']);
this.$refs.firstInput[0].focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr, i) => {
let qc;
switch (this.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
}
const param = this.localRoutine.parameters.find(param => `${i}-${param.name}` === curr); const firstInput: Ref<HTMLInputElement[]> = ref(null);
const values: Ref<{[key: string]: string}> = ref({});
const value = [...NUMBER, ...FLOAT].includes(param.type) ? this.values[curr] : `${qc}${this.values[curr]}${qc}`; const inParameters = computed(() => {
acc.push(value); return props.localRoutine.parameters.filter(param => param.context === 'IN');
return acc; });
}, []);
this.$emit('confirm', valArr); const typeClass = (type: string) => {
}, if (type)
closeModal () { return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
this.$emit('close'); return '';
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
}; };
const runRoutine = () => {
const valArr = Object.keys(values.value).reduce((acc, curr, i) => {
let qc;
switch (props.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
}
const param = props.localRoutine.parameters.find(param => `${i}-${param.name}` === curr);
const value = [...NUMBER, ...FLOAT].includes(param.type) ? values.value[curr] : `${qc}${values.value[curr]}${qc}`;
acc.push(value);
return acc;
}, []);
emit('confirm', valArr);
};
const closeModal = () => emit('close');
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
const wrapNumber = (num: number) => {
if (!num) return '';
return `(${num})`;
};
window.addEventListener('keydown', onKey);
setTimeout(() => {
firstInput.value[0].focus();
}, 20);
</script> </script>
<style scoped> <style scoped>

View File

@@ -2,8 +2,8 @@
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.discard')" :confirm-text="$t('word.discard')"
:cancel-text="$t('word.stay')" :cancel-text="$t('word.stay')"
@confirm="$emit('confirm')" @confirm="emit('confirm')"
@hide="$emit('close')" @hide="emit('close')"
> >
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
@@ -18,29 +18,23 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { onBeforeUnmount } from 'vue';
export default { const emit = defineEmits(['confirm', 'close']);
name: 'ModalDiscardChanges',
components: { const onKey = (e: KeyboardEvent) => {
ConfirmModal e.stopPropagation();
}, if (e.key === 'Escape')
emits: ['confirm', 'close'], emit('close');
created () {
window.addEventListener('keydown', this.onKey);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
}; };
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@@ -21,6 +21,7 @@
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
ref="firstInput"
v-model="database.name" v-model="database.name"
class="form-input" class="form-input"
type="text" type="text"
@@ -35,19 +36,13 @@
<label class="form-label">{{ $t('word.collation') }}</label> <label class="form-label">{{ $t('word.collation') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<select <BaseSelect
ref="firstInput"
v-model="database.collation" v-model="database.collation"
class="form-select" class="form-select"
> :options="collations"
<option option-label="collation"
v-for="collation in collations" option-track-by="collation"
:key="collation.id" />
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small> <small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div> </div>
</div> </div>
@@ -67,112 +62,102 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import BaseSelect from '@/components/BaseSelect.vue';
export default { const props = defineProps({
name: 'ModalEditSchema', selectedSchema: String
props: { });
selectedSchema: String
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const emit = defineEmits(['close']);
const { getWorkspace, getDatabaseVariable } = workspacesStore; const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
addNotification,
selectedWorkspace, const { getWorkspace, getDatabaseVariable } = workspacesStore;
getWorkspace,
getDatabaseVariable const { trapRef } = useFocusTrap();
};
}, const firstInput: Ref<HTMLInputElement> = ref(null);
data () { const database = ref({
return { name: '',
database: { prevName: '',
name: '', collation: '',
prevName: '', prevCollation: null
collation: '' });
}
}; const collations = computed(() => getWorkspace(selectedWorkspace.value).collations);
}, const defaultCollation = computed(() => (getDatabaseVariable(selectedWorkspace.value, 'collation_server').value || ''));
computed: {
collations () { const updateSchema = async () => {
return this.getWorkspace(this.selectedWorkspace).collations; if (database.value.collation !== database.value.prevCollation) {
},
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
}
},
async created () {
let actualCollation;
try { try {
const { status, response } = await Schema.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedSchema }); const { status, response } = await Schema.updateSchema({
uid: selectedWorkspace.value,
...database.value
});
if (status === 'success') if (status === 'success')
actualCollation = response; closeModal();
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
}
this.database = {
name: this.selectedSchema,
prevName: this.selectedSchema,
collation: actualCollation || this.defaultCollation,
prevCollation: actualCollation || this.defaultCollation
};
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async updateSchema () {
if (this.database.collation !== this.database.prevCollation) {
try {
const { status, response } = await Schema.updateSchema({
uid: this.selectedWorkspace,
...this.database
});
if (status === 'success')
this.closeModal();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
else
this.closeModal();
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
} }
} }
else closeModal();
}; };
const closeModal = () => emit('close');
const onKey =(e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
(async () => {
let actualCollation;
try {
const { status, response } = await Schema.getDatabaseCollation({ uid: selectedWorkspace.value, database: props.selectedSchema });
if (status === 'success')
actualCollation = response;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
database.value = {
name: props.selectedSchema,
prevName: props.selectedSchema,
collation: actualCollation || defaultCollation.value,
prevCollation: actualCollation || defaultCollation.value
};
window.addEventListener('keydown', onKey);
setTimeout(() => {
firstInput.value.focus();
}, 20);
})();
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@@ -146,7 +146,7 @@
<div class="tbody"> <div class="tbody">
<div <div
v-for="item in tables" v-for="item in tables"
:key="item.name" :key="item.table"
class="tr" class="tr"
> >
<div class="td"> <div class="td">
@@ -193,7 +193,7 @@
> >
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }} <input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }}
</label> </label>
<div v-if="customizations.exportByChunks"> <div v-if="clientCustoms.exportByChunks">
<div class="h6 mt-4 mb-2"> <div class="h6 mt-4 mb-2">
{{ $t('message.newInserStmtEvery') }}: {{ $t('message.newInserStmtEvery') }}:
</div> </div>
@@ -206,14 +206,11 @@
> >
</div> </div>
<div class="column col-6"> <div class="column col-6">
<select v-model="options.sqlInsertDivider" class="form-select"> <BaseSelect
<option value="bytes"> v-model="options.sqlInsertDivider"
KiB class="form-select"
</option> :options="[{value: 'bytes', label: 'KiB'}, {value: 'rows', label: $tc('word.row', 2)}]"
<option value="rows"> />
{{ $tc('word.row', 2) }}
</option>
</select>
</div> </div>
</div> </div>
</div> </div>
@@ -223,14 +220,11 @@
</div> </div>
<div class="columns"> <div class="columns">
<div class="column h5 mb-4"> <div class="column h5 mb-4">
<select v-model="options.outputFormat" class="form-select"> <BaseSelect
<option value="sql"> v-model="options.outputFormat"
{{ $t('message.singleFile', {ext: '.sql'}) }} class="form-select"
</option> :options="[{value: 'sql', label: $t('message.singleFile', {ext: '.sql'})}, {value: 'sql.zip', label: $t('message.zipCompressedFile', {ext: '.sql'})}]"
<option value="sql.zip"> />
{{ $t('message.zipCompressedFile', {ext: '.sql'}) }}
</option>
</select>
</div> </div>
</div> </div>
</div> </div>
@@ -269,207 +263,209 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import moment from 'moment'; import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import * as moment from 'moment';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { SchemaInfos } from 'common/interfaces/antares';
import { ExportOptions, ExportState, TableParams } from 'common/interfaces/exporter';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import customizations from 'common/customizations'; import { useFocusTrap } from '@/composables/useFocusTrap';
import Application from '@/ipc-api/Application'; import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { Customizations } from 'common/interfaces/customizations';
import BaseSelect from '@/components/BaseSelect.vue';
export default { const props = defineProps({
name: 'ModalExportSchema', selectedSchema: String
props: { });
selectedSchema: String
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const emit = defineEmits(['close']);
const { t } = useI18n();
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
getDatabaseVariable,
refreshSchema
} = workspacesStore;
return { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
addNotification,
selectedWorkspace,
getWorkspace,
getDatabaseVariable,
refreshSchema
};
},
data () {
return {
isExporting: false,
isRefreshing: false,
progressPercentage: 0,
progressStatus: '',
tables: [],
options: {
includes: {},
outputFormat: 'sql',
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes'
},
basePath: ''
};
},
computed: {
currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.currentWorkspace.customizations;
},
schemaItems () {
const db = this.currentWorkspace.structure.find(db => db.name === this.selectedSchema);
if (db)
return db.tables.filter(table => table.type === 'table');
return []; const { trapRef } = useFocusTrap();
},
filename () {
const date = moment().format('YYYY-MM-DD');
return `${this.selectedSchema}_${date}.${this.options.outputFormat}`;
},
dumpFilePath () {
return `${this.basePath}/${this.filename}`;
},
includeStructureStatus () {
if (this.tables.every(item => item.includeStructure)) return 1;
else if (this.tables.some(item => item.includeStructure)) return 2;
else return 0;
},
includeContentStatus () {
if (this.tables.every(item => item.includeContent)) return 1;
else if (this.tables.some(item => item.includeContent)) return 2;
else return 0;
},
includeDropStatementStatus () {
if (this.tables.every(item => item.includeDropStatement)) return 1;
else if (this.tables.some(item => item.includeDropStatement)) return 2;
else return 0;
}
},
async created () {
if (!this.schemaItems.length) await this.refresh();
window.addEventListener('keydown', this.onKey); const {
getWorkspace,
refreshSchema
} = workspacesStore;
this.basePath = await Application.getDownloadPathDirectory(); const isExporting = ref(false);
this.tables = this.schemaItems.map(item => ({ const isRefreshing = ref(false);
table: item.name, const progressPercentage = ref(0);
includeStructure: true, const progressStatus = ref('');
includeContent: true, const tables: Ref<TableParams[]> = ref([]);
includeDropStatement: true const options: Ref<ExportOptions> = ref({
})); schema: props.selectedSchema,
includes: {} as {[key: string]: boolean},
outputFile: '',
outputFormat: 'sql' as 'sql' | 'sql.zip',
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes' as 'bytes' | 'rows'
});
const basePath = ref('');
const structure = ['functions', 'views', 'triggers', 'routines', 'schedulers']; const currentWorkspace = computed(() => getWorkspace(selectedWorkspace.value));
const clientCustoms: Ref<Customizations> = computed(() => currentWorkspace.value.customizations);
const schemaItems = computed(() => {
const db: SchemaInfos = currentWorkspace.value.structure.find((db: SchemaInfos) => db.name === props.selectedSchema);
if (db)
return db.tables.filter(table => table.type === 'table');
structure.forEach(feat => { return [];
const val = customizations[this.currentWorkspace.client][feat]; });
if (val) const filename = computed(() => {
this.options.includes[feat] = true; const date = moment().format('YYYY-MM-DD');
}); return `${props.selectedSchema}_${date}.${options.value.outputFormat}`;
});
const dumpFilePath = computed(() => `${basePath.value}/${filename.value}`);
const includeStructureStatus = computed(() => {
if (tables.value.every(item => item.includeStructure)) return 1;
else if (tables.value.some(item => item.includeStructure)) return 2;
else return 0;
});
const includeContentStatus = computed(() => {
if (tables.value.every(item => item.includeContent)) return 1;
else if (tables.value.some(item => item.includeContent)) return 2;
else return 0;
});
const includeDropStatementStatus = computed(() => {
if (tables.value.every(item => item.includeDropStatement)) return 1;
else if (tables.value.some(item => item.includeDropStatement)) return 2;
else return 0;
});
ipcRenderer.on('export-progress', this.updateProgress); const startExport = async () => {
}, isExporting.value = true;
beforeUnmount () { const { uid, client } = currentWorkspace.value;
window.removeEventListener('keydown', this.onKey); const params = {
ipcRenderer.off('export-progress', this.updateProgress); uid,
}, type: client,
methods: { schema: props.selectedSchema,
async startExport () { outputFile: dumpFilePath.value,
this.isExporting = true; tables: [...tables.value],
const { uid, client } = this.currentWorkspace; ...options.value
const params = { };
uid,
type: client,
schema: this.selectedSchema,
outputFile: this.dumpFilePath,
tables: [...this.tables],
...this.options
};
try { try {
const { status, response } = await Schema.export(params); const { status, response } = await Schema.export(params);
if (status === 'success') if (status === 'success')
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed'); progressStatus.value = response.cancelled ? t('word.aborted') : t('word.completed');
else { else {
this.progressStatus = response; progressStatus.value = response;
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isExporting = false;
},
updateProgress (event, state) {
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':
this.progressStatus = this.$t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
this.progressStatus = this.$t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
this.progressStatus = this.$t('message.writingTableExport', { table: state.currentItem });
break;
}
},
async closeModal () {
let willClose = true;
if (this.isExporting) {
willClose = false;
const { response } = await Schema.abortExport();
willClose = response.willAbort;
}
if (willClose)
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
checkAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: true, includeContent: true, includeDropStatement: true }));
},
uncheckAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: false, includeContent: false, includeDropStatement: false }));
},
toggleAllTablesOption (option) {
const options = ['includeStructure', 'includeContent', 'includeDropStatement'];
if (!options.includes(option)) return;
if (this[`${option}Status`] !== 1)
this.tables = this.tables.map(item => ({ ...item, [option]: true }));
else
this.tables = this.tables.map(item => ({ ...item, [option]: false }));
},
async refresh () {
this.isRefreshing = true;
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
this.isRefreshing = false;
},
async openPathDialog () {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled)
this.basePath = result.filePaths[0];
} }
} }
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isExporting.value = false;
}; };
const updateProgress = (event: Event, state: ExportState) => {
progressPercentage.value = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':
progressStatus.value = t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
progressStatus.value = t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
progressStatus.value = t('message.writingTableExport', { table: state.currentItem });
break;
}
};
const closeModal = async () => {
let willClose = true;
if (isExporting.value) {
willClose = false;
const { response } = await Schema.abortExport();
willClose = response.willAbort;
}
if (willClose)
emit('close');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
const checkAllTables = () => {
tables.value = tables.value.map(item => ({ ...item, includeStructure: true, includeContent: true, includeDropStatement: true }));
};
const uncheckAllTables = () => {
tables.value = tables.value.map(item => ({ ...item, includeStructure: false, includeContent: false, includeDropStatement: false }));
};
const toggleAllTablesOption = (option: 'includeStructure' | 'includeContent' |'includeDropStatement') => {
const options = {
includeStructure: includeStructureStatus.value,
includeContent: includeContentStatus.value,
includeDropStatement: includeDropStatementStatus.value
};
if (options[option] !== 1)
tables.value = tables.value.map(item => ({ ...item, [option]: true }));
else
tables.value = tables.value.map(item => ({ ...item, [option]: false }));
};
const refresh = async () => {
isRefreshing.value = true;
await refreshSchema({ uid: currentWorkspace.value.uid, schema: props.selectedSchema });
isRefreshing.value = false;
};
const openPathDialog = async () => {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled)
basePath.value = result.filePaths[0];
};
(async () => {
if (!schemaItems.value.length) await refresh();
window.addEventListener('keydown', onKey);
basePath.value = await Application.getDownloadPathDirectory();
tables.value = schemaItems.value.map(item => ({
table: item.name,
includeStructure: true,
includeContent: true,
includeDropStatement: true
}));
const structure = ['functions', 'views', 'triggers', 'routines', 'schedulers'];
structure.forEach((feat: keyof Customizations) => {
const val = clientCustoms.value[feat];
if (val)
options.value.includes[feat] = true;
});
ipcRenderer.on('export-progress', updateProgress);
})();
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
ipcRenderer.off('export-progress', updateProgress);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -478,14 +474,15 @@ export default {
overflow: hidden; overflow: hidden;
.left { .left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
} }
} }
.workspace-query-results { .workspace-query-results {
flex: 1 0 1px; flex: 1 0 1px;
.table { .table {
width: 100% !important; width: 100% !important;
} }
@@ -501,25 +498,24 @@ export default {
} }
.modal { .modal {
.modal-container { .modal-container {
max-width: 800px; max-width: 800px;
} }
.modal-body { .modal-body {
max-height: 60vh; max-height: 60vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.modal-footer { .modal-footer {
display: flex; display: flex;
} }
} }
.progress-status { .progress-status {
font-style: italic; font-style: italic;
font-size: 80%; font-size: 80%;
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@@ -72,99 +72,11 @@
class="tooltip tooltip-right ml-2" class="tooltip tooltip-right ml-2"
:data-tooltip="$t('message.fakeDataLanguage')" :data-tooltip="$t('message.fakeDataLanguage')"
> >
<select v-model="fakerLocale" class="form-select"> <BaseSelect
<option value="ar"> v-model="fakerLocale"
Arabic :options="locales"
</option><option value="az"> class="form-select"
Azerbaijani />
</option><option value="zh_CN">
Chinese
</option><option value="zh_TW">
Chinese (Taiwan)
</option><option value="cz">
Czech
</option><option value="nl">
Dutch
</option><option value="nl_BE">
Dutch (Belgium)
</option><option value="en">
English
</option><option value="en_AU_ocker">
English (Australia Ocker)
</option><option value="en_AU">
English (Australia)
</option><option value="en_BORK">
English (Bork)
</option><option value="en_CA">
English (Canada)
</option><option value="en_GB">
English (Great Britain)
</option><option value="en_IND">
English (India)
</option><option value="en_IE">
English (Ireland)
</option><option value="en_ZA">
English (South Africa)
</option><option value="en_US">
English (United States)
</option><option value="fa">
Farsi
</option><option value="fi">
Finnish
</option><option value="fr">
French
</option><option value="fr_CA">
French (Canada)
</option><option value="fr_CH">
French (Switzerland)
</option><option value="ge">
Georgian
</option><option value="de">
German
</option><option value="de_AT">
German (Austria)
</option><option value="de_CH">
German (Switzerland)
</option><option value="hr">
Hrvatski
</option><option value="id_ID">
Indonesia
</option><option value="it">
Italian
</option><option value="ja">
Japanese
</option><option value="ko">
Korean
</option><option value="nep">
Nepalese
</option><option value="nb_NO">
Norwegian
</option><option value="pl">
Polish
</option><option value="pt_BR">
Portuguese (Brazil)
</option><option value="pt_PT">
Portuguese (Portugal)
</option><option value="ro">
Romanian
</option><option value="ru">
Russian
</option><option value="sk">
Slovakian
</option><option value="es">
Spanish
</option><option value="es_MX">
Spanish (Mexico)
</option><option value="sv">
Swedish
</option><option value="tr">
Turkish
</option><option value="uk">
Ukrainian
</option><option value="vi">
Vietnamese
</option>
</select>
</div> </div>
</div> </div>
<div class="column col-auto"> <div class="column col-auto">
@@ -185,201 +97,239 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import moment from 'moment'; import { computed, onBeforeMount, onMounted, Prop, Ref, ref, watch } from 'vue';
import * as moment from 'moment';
import { TableField, TableForeign } from 'common/interfaces/antares';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect'; import FakerSelect from '@/components/FakerSelect.vue';
import BaseSelect from '@/components/BaseSelect.vue';
export default { const props = defineProps({
name: 'ModalFakerRows', tabUid: [String, Number],
components: { fields: Array as Prop<TableField[]>,
FakerSelect keyUsage: Array as Prop<TableForeign[]>
}, });
props: {
tabUid: [String, Number],
fields: Array,
keyUsage: Array
},
emits: ['reload', 'hide'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const emit = defineEmits(['reload', 'hide']);
const { getWorkspace, getWorkspaceTab } = workspacesStore; const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
addNotification,
selectedWorkspace,
getWorkspace,
getWorkspaceTab
};
},
data () {
return {
localRow: {},
fieldsToExclude: [],
nInserts: 1,
isInserting: false,
fakerLocale: 'en'
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
foreignKeys () {
return this.keyUsage.map(key => key.field);
},
hasFakes () {
return Object.keys(this.localRow).some(field => 'group' in this.localRow[field] && this.localRow[field].group !== 'manual');
}
},
watch: {
nInserts (val) {
if (!val || val < 1)
this.nInserts = 1;
else if (val > 1000)
this.nInserts = 1000;
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
const rowObj = {};
for (const field of this.fields) { const { getWorkspace } = workspacesStore;
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null; const { trapRef } = useFocusTrap({ disableAutofocus: true });
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = !field.default || Number.isNaN(+field.default.replaceAll('\'', '')) ? null : +field.default.replaceAll('\'', '');
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = field.default
? field.default.includes('\'')
? field.default.split('\'')[1]
: field.default
: '';
}
else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.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()'].some(term => field.default.toLowerCase().includes(term))) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
else
fieldDefault = field.default;
}
else if (field.enumValues)
fieldDefault = field.enumValues.replaceAll('\'', '').split(',');
else
fieldDefault = field.default;
}
rowObj[field.name] = { value: fieldDefault }; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const localRow: Ref<{[key: string]: any}> = ref({});
const fieldsToExclude = ref([]);
const nInserts = ref(1);
const isInserting = ref(false);
const fakerLocale = ref('en');
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields const workspace = computed(() => getWorkspace(selectedWorkspace.value));
this.fieldsToExclude = [...this.fieldsToExclude, field.name]; const foreignKeys = computed(() => props.keyUsage.map(key => key.field));
} const hasFakes = computed(() => Object.keys(localRow.value).some(field => 'group' in localRow.value[field] && localRow.value[field].group !== 'manual'));
this.localRow = { ...rowObj }; const locales = [
}, { value: 'ar', label: 'Arabic' },
beforeUnmount () { { value: 'az', label: 'Azerbaijani' },
window.removeEventListener('keydown', this.onKey); { value: 'zh_CN', label: 'Chinese' },
}, { value: 'zh_TW', label: 'Chinese (Taiwan)' },
methods: { { value: 'cz', label: 'Czech' },
typeClass (type) { { value: 'nl', label: 'Dutch' },
if (type) { value: 'nl_BE', label: 'Dutch (Belgium)' },
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`; { value: 'en', label: 'English' },
return ''; { value: 'en_AU_ocker', label: 'English (Australia Ocker)' },
}, { value: 'en_AU', label: 'English (Australia)' },
async insertRows () { { value: 'en_BORK', label: 'English (Bork)' },
this.isInserting = true; { value: 'en_CA', label: 'English (Canada)' },
const rowToInsert = this.localRow; { value: 'en_GB', label: 'English (Great Britain)' },
{ value: 'en_IND', label: 'English (India)' },
{ value: 'en_IE', label: 'English (Ireland)' },
{ value: 'en_ZA', label: 'English (South Africa)' },
{ value: 'en_US', label: 'English (United States)' },
{ value: 'fa', label: 'Farsi' },
{ value: 'fi', label: 'Finnish' },
{ value: 'fr', label: 'French' },
{ value: 'fr_CA', label: 'French (Canada)' },
{ value: 'fr_CH', label: 'French (Switzerland)' },
{ value: 'ge', label: 'Georgian' },
{ value: 'de', label: 'German' },
{ value: 'de_AT', label: 'German (Austria)' },
{ value: 'de_CH', label: 'German (Switzerland)' },
{ value: 'hr', label: 'Hrvatski' },
{ value: 'id_ID', label: 'Indonesia' },
{ value: 'it', label: 'Italian' },
{ value: 'ja', label: 'Japanese' },
{ value: 'ko', label: 'Korean' },
{ value: 'nep', label: 'Nepalese' },
{ value: 'nb_NO', label: 'Norwegian' },
{ value: 'pl', label: 'Polish' },
{ value: 'pt_BR', label: 'Portuguese (Brazil)' },
{ value: 'pt_PT', label: 'Portuguese (Portugal)' },
{ value: 'ro', label: 'Romanian' },
{ value: 'ru', label: 'Russian' },
{ value: 'sk', label: 'Slovakian' },
{ value: 'es', label: 'Spanish' },
{ value: 'es_MX', label: 'Spanish (Mexico)' },
{ value: 'sv', label: 'Swedish' },
{ value: 'tr', label: 'Turkish' },
{ value: 'uk', label: 'Ukrainian' },
{ value: 'vi', label: 'Vietnamese' }
Object.keys(rowToInsert).forEach(key => { ];
if (this.fieldsToExclude.includes(key))
delete rowToInsert[key];
if (typeof rowToInsert[key] === 'undefined') watch(nInserts, (val) => {
delete rowToInsert[key]; if (!val || val < 1)
}); nInserts.value = 1;
else if (val > 1000)
nInserts.value = 1000;
});
const fieldTypes = {}; const typeClass = (type: string) => {
this.fields.forEach(field => { if (type)
fieldTypes[field.name] = field.type; return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
}); return '';
try {
const { status, response } = await Tables.insertTableFakeRows({
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table,
row: rowToInsert,
repeat: this.nInserts,
fields: fieldTypes,
locale: this.fakerLocale
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isInserting = false;
},
closeModal () {
this.$emit('hide');
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return Number(field.charLength);
return Number(field.length);
},
toggleFields (event, field) {
if (event.target.checked)
this.fieldsToExclude = this.fieldsToExclude.filter(f => f !== field.name);
else
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
},
filesChange (event, field) {
const { files } = event.target;
if (!files.length) return;
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.field === keyName);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
}; };
const insertRows = async () => {
isInserting.value = true;
const rowToInsert = localRow.value;
Object.keys(rowToInsert).forEach(key => {
if (fieldsToExclude.value.includes(key))
delete rowToInsert[key];
if (typeof rowToInsert[key] === 'undefined')
delete rowToInsert[key];
});
const fieldTypes: {[key: string]: string} = {};
props.fields.forEach(field => {
fieldTypes[field.name] = field.type;
});
try {
const { status, response } = await Tables.insertTableFakeRows({
uid: selectedWorkspace.value,
schema: workspace.value.breadcrumbs.schema,
table: workspace.value.breadcrumbs.table,
row: rowToInsert,
repeat: nInserts.value,
fields: fieldTypes,
locale: fakerLocale.value
});
if (status === 'success') {
closeModal();
emit('reload');
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isInserting.value = false;
};
const closeModal = () => {
emit('hide');
};
const fieldLength = (field: TableField) => {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return Number(field.charLength);
return Number(field.length);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const toggleFields = (event: any, field: TableField) => {
if (event.target.checked)
fieldsToExclude.value = fieldsToExclude.value.filter(f => f !== field.name);
else
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
const wrapNumber = (num: number) => {
if (!num) return '';
return `(${num})`;
};
window.addEventListener('keydown', onKey);
onMounted(() => {
setTimeout(() => {
const inputs = Array.from(document.querySelectorAll<HTMLInputElement>('.modal-container .form-input'));
if (inputs?.length) {
const firstEnabledInput = inputs.find((el) => !el.disabled);
firstEnabledInput?.focus();
}
}, 50);
const rowObj: {[key: string]: unknown} = {};
for (const field of props.fields) {
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = !field.default || Number.isNaN(+field.default.replaceAll('\'', '')) ? null : +field.default.replaceAll('\'', '');
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = field.default
? field.default.includes('\'')
? field.default.split('\'')[1]
: field.default
: '';
}
else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.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()'].some(term => field.default.toLowerCase().includes(term))) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
else
fieldDefault = field.default;
}
else if (field.enumValues)
fieldDefault = field.enumValues.replaceAll('\'', '').split(',');
else
fieldDefault = field.default;
}
rowObj[field.name] = { value: fieldDefault };
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
}
localRow.value = { ...rowObj };
});
onBeforeMount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -2,12 +2,12 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4"> <div ref="trapRef" class="modal-container p-0 pb-4">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-history mr-1" /> <i class="mdi mdi-24px mdi-history mr-1" />
<span class="cut-text">{{ $t('word.history') }}: {{ connectionName }}</span> <span class="cut-text">{{ t('word.history') }}: {{ connectionName }}</span>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -22,7 +22,7 @@
v-model="searchTerm" v-model="searchTerm"
class="form-input" class="form-input"
type="text" type="text"
:placeholder="$t('message.searchForQueries')" :placeholder="t('message.searchForQueries')"
> >
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" /> <i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" />
<i <i
@@ -67,13 +67,13 @@
<small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small> <small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small>
<div class="tile-history-buttons"> <div class="tile-history-buttons">
<button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)"> <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') }} <i class="mdi mdi-open-in-app pr-1" /> {{ t('word.select') }}
</button> </button>
<button class="btn btn-link pl-1" @click="copyQuery(query.sql)"> <button class="btn btn-link pl-1" @click="copyQuery(query.sql)">
<i class="mdi mdi-content-copy pr-1" /> {{ $t('word.copy') }} <i class="mdi mdi-content-copy pr-1" /> {{ t('word.copy') }}
</button> </button>
<button class="btn btn-link pl-1" @click="deleteQuery(query)"> <button class="btn btn-link pl-1" @click="deleteQuery(query)">
<i class="mdi mdi-delete-forever pr-1" /> {{ $t('word.delete') }} <i class="mdi mdi-delete-forever pr-1" /> {{ t('word.delete') }}
</button> </button>
</div> </div>
</div> </div>
@@ -88,7 +88,7 @@
<i class="mdi mdi-history mdi-48px" /> <i class="mdi mdi-history mdi-48px" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.thereIsNoQueriesYet') }} {{ t('message.thereIsNoQueriesYet') }}
</p> </p>
</div> </div>
</div> </div>
@@ -97,129 +97,114 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import moment from 'moment'; import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue';
import { useHistoryStore } from '@/stores/history'; import { useI18n } from 'vue-i18n';
import * as moment from 'moment';
import { ConnectionParams } from 'common/interfaces/antares';
import { HistoryRecord, useHistoryStore } from '@/stores/history';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useFocusTrap } from '@/composables/useFocusTrap';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
export default { const { t } = useI18n();
name: 'ModalHistory',
components: {
BaseVirtualScroll
},
props: {
connection: Object
},
emits: ['select-query', 'close'],
setup () {
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
return { const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
getHistoryByWorkspace, const { getConnectionName } = useConnectionsStore();
deleteQueryFromHistory,
getConnectionName,
addNotification
};
},
data () {
return {
resultsSize: 1000,
isQuering: false,
scrollElement: null,
searchTermInterval: null,
searchTerm: '',
localSearchTerm: ''
};
},
computed: {
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(() => { const { trapRef } = useFocusTrap();
this.localSearchTerm = this.searchTerm;
}, 200);
}
},
created () {
window.addEventListener('keydown', this.onKey, { capture: true });
},
updated () {
if (this.$refs.table)
this.refreshScroller();
if (this.$refs.tableWrapper) const props = defineProps({
this.scrollElement = this.$refs.tableWrapper; connection: Object as Prop<ConnectionParams>
}, });
mounted () {
this.resizeResults();
window.addEventListener('resize', this.resizeResults);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
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) const emit = defineEmits(['select-query', 'close']);
this.resultsSize = el.offsetHeight - this.$refs.searchForm.offsetHeight;
this.$refs.resultTable.updateWindow(); const table: Ref<HTMLDivElement> = ref(null);
} const resultTable: Ref<Component & { updateWindow: () => void }> = ref(null);
}, const tableWrapper: Ref<HTMLDivElement> = ref(null);
formatDate (date) { const searchForm: Ref<HTMLInputElement> = ref(null);
return moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date; const resultsSize = ref(1000);
}, const scrollElement: Ref<HTMLDivElement> = ref(null);
refreshScroller () { const searchTermInterval: Ref<NodeJS.Timeout> = ref(null);
this.resizeResults(); const searchTerm = ref('');
}, const localSearchTerm = ref('');
closeModal () {
this.$emit('close');
},
highlightWord (string) {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (this.searchTerm) { const connectionName = computed(() => getConnectionName(props.connection.uid));
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || []));
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>'); const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(searchTerm.value.toLowerCase()) >= 0));
}
else watch(searchTerm, () => {
return string; clearTimeout(searchTermInterval.value);
},
onKey (e) { searchTermInterval.value = setTimeout(() => {
e.stopPropagation(); localSearchTerm.value = searchTerm.value;
if (e.key === 'Escape') }, 200);
this.closeModal(); });
}
const copyQuery = (sql: string) => {
navigator.clipboard.writeText(sql);
};
const deleteQuery = (query: HistoryRecord[]) => {
deleteQueryFromHistory({
workspace: props.connection.uid,
...query
});
};
const resizeResults = () => {
if (resultTable.value) {
const el = tableWrapper.value.parentElement;
if (el)
resultsSize.value = el.offsetHeight - searchForm.value.offsetHeight;
resultTable.value.updateWindow();
} }
}; };
const formatDate = (date: Date) => moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date;
const refreshScroller = () => resizeResults();
const closeModal = () => emit('close');
const highlightWord = (string: string) => {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (searchTerm.value) {
const regexp = new RegExp(`(${searchTerm.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
}
else
return string;
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
window.addEventListener('keydown', onKey, { capture: true });
onUpdated(() => {
if (table.value)
refreshScroller();
if (tableWrapper.value)
scrollElement.value = tableWrapper.value;
});
onMounted(() => {
resizeResults();
window.addEventListener('resize', resizeResults);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey, { capture: true });
window.removeEventListener('resize', resizeResults);
clearInterval(searchTermInterval.value);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -49,146 +49,140 @@
</teleport> </teleport>
</template> </template>
<script> <script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import moment from 'moment';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n';
import { ImportState } from 'common/interfaces/importer';
export default { const { t } = useI18n();
name: 'ModalImportSchema',
props: { const { addNotification } = useNotificationsStore();
selectedSchema: String const workspacesStore = useWorkspacesStore();
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, refreshSchema } = workspacesStore; const { getWorkspace, refreshSchema } = workspacesStore;
return { const props = defineProps({
addNotification, selectedSchema: String
selectedWorkspace, });
getWorkspace,
refreshSchema const emit = defineEmits(['close']);
};
}, const sqlFile = ref('');
data () { const isImporting = ref(false);
return { const progressPercentage = ref(0);
sqlFile: '', const queryCount = ref(0);
isImporting: false, const completed = ref(false);
progressPercentage: 0, const progressStatus = ref('Reading');
queryCount: 0, const queryErrors: Ref<{time: string; message: string}[]> = ref([]);
completed: false,
progressStatus: 'Reading', const currentWorkspace = computed(() => getWorkspace(selectedWorkspace.value));
queryErrors: []
}; const formattedQueryErrors = computed(() => {
}, return queryErrors.value.map(err =>
computed: { `Time: ${moment(err.time).format('HH:mm:ss.S')} (${err.time})\nError: ${err.message}`
currentWorkspace () { ).join('\n\n');
return this.getWorkspace(this.selectedWorkspace); });
},
formattedQueryErrors () { const startImport = async (file: string) => {
return this.queryErrors.map(err => isImporting.value = true;
`Time: ${moment(err.time).format('HH:mm:ss.S')} (${err.time})\nError: ${err.message}` sqlFile.value = file;
).join('\n\n');
} const { uid, client } = currentWorkspace.value;
}, const params = {
async created () { uid,
window.addEventListener('keydown', this.onKey); type: client,
schema: props.selectedSchema,
ipcRenderer.on('import-progress', this.updateProgress); file: sqlFile.value
ipcRenderer.on('query-error', this.handleQueryError); };
},
beforeUnmount () { try {
window.removeEventListener('keydown', this.onKey); completed.value = false;
ipcRenderer.off('import-progress', this.updateProgress); const { status, response } = await Schema.import(params);
ipcRenderer.off('query-error', this.handleQueryError);
}, if (status === 'success')
methods: { progressStatus.value = response.cancelled ? t('word.aborted') : t('word.completed');
async startImport (sqlFile) { else {
this.isImporting = true; progressStatus.value = response;
this.sqlFile = sqlFile; addNotification({ status: 'error', message: response });
const { uid, client } = this.currentWorkspace;
const params = {
uid,
type: client,
schema: this.selectedSchema,
file: sqlFile
};
try {
this.completed = false;
const { status, response } = await Schema.import(params);
if (status === 'success')
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else {
this.progressStatus = response;
this.addNotification({ status: 'error', message: response });
}
this.refreshSchema({ uid, schema: this.selectedSchema });
this.completed = true;
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isImporting = false;
},
updateProgress (event, state) {
this.progressPercentage = Number(state.percentage).toFixed(1);
this.queryCount = Number(state.queryCount);
},
handleQueryError (event, err) {
this.queryErrors.push(err);
},
async closeModal () {
let willClose = true;
if (this.isImporting) {
willClose = false;
const { response } = await Schema.abortImport();
willClose = response.willAbort;
}
if (willClose)
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
} }
refreshSchema({ uid, schema: props.selectedSchema });
completed.value = true;
} }
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isImporting.value = false;
}; };
const updateProgress = (event: Event, state: ImportState) => {
progressPercentage.value = parseFloat(Number(state.percentage).toFixed(1));
queryCount.value = Number(state.queryCount);
};
const handleQueryError = (event: Event, err: { time: string; message: string }) => {
queryErrors.value.push(err);
};
const closeModal = async () => {
let willClose = true;
if (isImporting.value) {
willClose = false;
const { response } = await Schema.abortImport();
willClose = response.willAbort;
}
if (willClose)
emit('close');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
window.addEventListener('keydown', onKey);
ipcRenderer.on('import-progress', updateProgress);
ipcRenderer.on('query-error', handleQueryError);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
ipcRenderer.off('import-progress', updateProgress);
ipcRenderer.off('query-error', handleQueryError);
});
defineExpose({ startImport });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.modal { .modal {
.modal-container {
max-width: 800px;
}
.modal-container { .modal-body {
max-width: 800px; max-height: 60vh;
} display: flex;
flex-direction: column;
}
.modal-body { .modal-footer {
max-height: 60vh; display: flex;
display: flex; }
flex-direction: column;
}
.modal-footer {
display: flex;
}
} }
.progress-status { .progress-status {
font-style: italic; font-style: italic;
font-size: 80%; font-size: 80%;
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div class="modal active"> <div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0"> <div ref="trapRef" class="modal-container p-0">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@@ -35,15 +35,13 @@
<label class="form-label">{{ $t('word.collation') }}</label> <label class="form-label">{{ $t('word.collation') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<select v-model="database.collation" class="form-select"> <BaseSelect
<option v-model="database.collation"
v-for="collation in collations" class="form-select"
:key="collation.id" :options="collations"
:value="collation.collation" option-label="collation"
> option-track-by="collation"
{{ collation.collation }} />
</option>
</select>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small> <small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div> </div>
</div> </div>
@@ -67,91 +65,78 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { storeToRefs } from 'pinia'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { addNotification } = useNotificationsStore();
name: 'ModalNewSchema', const workspacesStore = useWorkspacesStore();
emits: ['reload', 'close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getDatabaseVariable } = workspacesStore; const { getWorkspace, getDatabaseVariable } = workspacesStore;
return { const { trapRef } = useFocusTrap();
addNotification,
selectedWorkspace, const emit = defineEmits(['reload', 'close']);
getWorkspace,
getDatabaseVariable const firstInput: Ref<HTMLInputElement> = ref(null);
}; const isLoading = ref(false);
}, const database = ref({
data () { name: '',
return { collation: ''
isLoading: false, });
database: {
name: '', const collations = computed(() => getWorkspace(selectedWorkspace.value).collations);
collation: '' const customizations = computed(() => getWorkspace(selectedWorkspace.value).customizations);
} const defaultCollation = computed(() => getDatabaseVariable(selectedWorkspace.value, 'collation_server') ? getDatabaseVariable(selectedWorkspace.value, 'collation_server').value : '');
};
}, database.value = { ...database.value, collation: defaultCollation.value };
computed: {
collations () { const createSchema = async () => {
return this.getWorkspace(this.selectedWorkspace).collations; isLoading.value = true;
}, try {
customizations () { const { status, response } = await Schema.createSchema({
return this.getWorkspace(this.selectedWorkspace).customizations; uid: selectedWorkspace.value,
}, ...database.value
defaultCollation () { });
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server') ? this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value : '';
} if (status === 'success') {
}, closeModal();
created () { emit('reload');
this.database = { ...this.database, collation: this.defaultCollation };
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async createSchema () {
this.isLoading = true;
try {
const { status, response } = await Schema.createSchema({
uid: this.selectedWorkspace,
...this.database
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isLoading = false;
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
} }
else
addNotification({ status: 'error', message: response });
} }
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isLoading.value = false;
}; };
const closeModal = () => {
emit('close');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
window.addEventListener('keydown', onKey);
setTimeout(() => {
firstInput.value.focus();
}, 20);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,366 +0,0 @@
<template>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $t('message.addNewRow') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset :disabled="isInserting">
<div
v-for="(field, key) in fields"
:key="field.name"
class="form-group"
>
<div class="col-4 col-sm-12">
<label class="form-label" :title="field.name">{{ field.name }}</label>
</div>
<div class="input-group col-8 col-sm-12">
<ForeignKeySelect
v-if="foreignKeys.includes(field.name)"
ref="formInput"
v-model="localRow[field.name]"
class="form-select"
:key-usage="getKeyUsage(field.name)"
:disabled="fieldsToExclude.includes(field.name)"
/>
<input
v-else-if="inputProps(field).mask"
ref="formInput"
v-model="localRow[field.name]"
v-mask="inputProps(field).mask"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else-if="inputProps(field).type === 'file'"
ref="formInput"
class="form-input"
type="file"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
@change="filesChange($event,field.name)"
>
<input
v-else-if="inputProps(field).type === 'number'"
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
step="any"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<span class="input-group-addon" :class="typeCLass(field.type)">
{{ field.type }} {{ wrapNumber(fieldLength(field)) }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!field.autoIncrement"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="modal-footer">
<div class="input-group col-3 tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<input
v-model="nInserts"
type="number"
class="form-input"
min="1"
:disabled="isInserting"
>
<span class="input-group-addon">
<i class="mdi mdi-24px mdi-repeat" />
</span>
</div>
<div>
<button
class="btn btn-primary mr-2"
:class="{'loading': isInserting}"
@click.stop="insertRows"
>
{{ $t('word.insert') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect';
import { storeToRefs } from 'pinia';
export default {
name: 'ModalNewTableRow',
components: {
ForeignKeySelect
},
props: {
tabUid: [String, Number],
fields: Array,
keyUsage: Array
},
emits: ['reload', 'hide'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getWorkspaceTab } = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
getWorkspaceTab
};
},
data () {
return {
localRow: {},
fieldsToExclude: [],
nInserts: 1,
isInserting: false
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
foreignKeys () {
return this.keyUsage.map(key => key.field);
}
},
watch: {
nInserts (val) {
if (!val || val < 1)
this.nInserts = 1;
else if (val > 1000)
this.nInserts = 1000;
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
const rowObj = {};
for (const field of this.fields) {
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = +field.default;
if ([...TEXT, ...LONG_TEXT].includes(field.type))
fieldDefault = field.default ? field.default.substring(1, field.default.length - 1) : '';
if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
if (DATETIME.includes(field.type)) {
if (field.default && field.default.toLowerCase().includes('current_timestamp')) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
}
}
rowObj[field.name] = fieldDefault;
if (field.autoIncrement)// Disable by default auto increment fields
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
}
this.localRow = { ...rowObj };
// Auto focus
setTimeout(() => {
const firstSelectableInput = this.$refs.formInput.find(input => !input.disabled);
firstSelectableInput.focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () {
this.isInserting = true;
const rowToInsert = this.localRow;
Object.keys(rowToInsert).forEach(key => {
if (this.fieldsToExclude.includes(key))
delete rowToInsert[key];
if (typeof rowToInsert[key] === 'undefined')
delete rowToInsert[key];
});
const fieldTypes = {};
this.fields.forEach(field => {
fieldTypes[field.name] = field.type;
});
try {
const { status, response } = await Tables.insertTableRows({
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table,
row: rowToInsert,
repeat: this.nInserts,
fields: fieldTypes
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isInserting = false;
},
closeModal () {
this.$emit('hide');
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return field.charLength;
return field.length;
},
inputProps (field) {
if ([...TEXT, ...LONG_TEXT].includes(field.type))
return { type: 'text', mask: false };
if ([...NUMBER, ...FLOAT].includes(field.type))
return { type: 'number', mask: false };
if (TIME.includes(field.type)) {
let timeMask = '##:##:##';
const precision = this.fieldLength(field);
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: timeMask };
}
if (DATE.includes(field.type))
return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(field.type)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.fieldLength(field);
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
if (BLOB.includes(field.type))
return { type: 'file', mask: false };
if (BIT.includes(field.type))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
},
toggleFields (event, field) {
if (event.target.checked)
this.fieldsToExclude = this.fieldsToExclude.filter(f => f !== field.name);
else
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
},
filesChange (event, field) {
const { files } = event.target;
if (!files.length) return;
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.field === keyName);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
};
</script>
<style scoped>
.modal-container {
max-width: 500px;
}
.form-label {
overflow: hidden;
white-space: normal;
text-overflow: ellipsis;
}
.input-group-addon {
display: flex;
align-items: center;
}
.modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -12,7 +12,7 @@
@close-context="closeContext" @close-context="closeContext"
/> />
<a class="modal-overlay" @click.stop="closeModal" /> <a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4"> <div ref="trapRef" class="modal-container p-0 pb-4">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
@@ -133,218 +133,221 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import arrayToFile from '../libs/arrayToFile'; import { Component, computed, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref } from 'vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { arrayToFile } from '../libs/arrayToFile';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import ModalProcessesListRow from '@/components/ModalProcessesListRow'; import ModalProcessesListRow from '@/components/ModalProcessesListRow.vue';
import ModalProcessesListContext from '@/components/ModalProcessesListContext'; import ModalProcessesListContext from '@/components/ModalProcessesListContext.vue';
export default { const { addNotification } = useNotificationsStore();
name: 'ModalProcessesList', const { getConnectionName } = useConnectionsStore();
components: {
BaseVirtualScroll,
ModalProcessesListRow,
ModalProcessesListContext
},
props: {
connection: Object
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const { getConnectionName } = useConnectionsStore();
return { addNotification, getConnectionName }; const { trapRef } = useFocusTrap();
},
data () { const props = defineProps({
return { connection: Object as Prop<ConnectionParams>
resultsSize: 1000, });
isQuering: false,
isContext: false, const emit = defineEmits(['close']);
autorefreshTimer: 0,
refreshInterval: null, const tableWrapper: Ref<HTMLDivElement> = ref(null);
contextEvent: null, const table: Ref<HTMLDivElement> = ref(null);
selectedCell: null, const resultTable: Ref<Component & {updateWindow: () => void}> = ref(null);
selectedRow: null, const resultsSize = ref(1000);
results: [], const isQuering = ref(false);
fields: [], const isContext = ref(false);
currentSort: '', const autorefreshTimer = ref(0);
currentSortDir: 'asc', const refreshInterval: Ref<NodeJS.Timeout> = ref(null);
scrollElement: null const contextEvent = ref(null);
}; const selectedCell = ref(null);
}, const selectedRow: Ref<number> = ref(null);
computed: { const results = ref([]);
connectionName () { const fields = ref([]);
return this.getConnectionName(this.connection.uid); const currentSort = ref('');
}, const currentSortDir = ref('asc');
sortedResults () { const scrollElement = ref(null);
if (this.currentSort) {
return [...this.results].sort((a, b) => { const connectionName = computed(() => getConnectionName(props.connection.uid));
let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort]; const sortedResults = computed(() => {
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort]; if (currentSort.value) {
if (this.currentSortDir === 'desc') modifier = -1; return [...results.value].sort((a, b) => {
if (valA < valB) return -1 * modifier; let modifier = 1;
if (valA > valB) return 1 * modifier; const valA = typeof a[currentSort.value] === 'string' ? a[currentSort.value].toLowerCase() : a[currentSort.value];
return 0; const valB = typeof b[currentSort.value] === 'string' ? b[currentSort.value].toLowerCase() : b[currentSort.value];
}); if (currentSortDir.value === 'desc') modifier = -1;
} if (valA < valB) return -1 * modifier;
else if (valA > valB) return 1 * modifier;
return this.results; return 0;
});
}
else
return results.value;
});
const getProcessesList = async () => {
isQuering.value = true;
try { // Table data
const { status, response } = await Schema.getProcesses(props.connection.uid);
if (status === 'success') {
results.value = response;
fields.value = response.length ? Object.keys(response[0]) : [];
} }
}, else
created () { addNotification({ status: 'error', message: response });
window.addEventListener('keydown', this.onKey, { capture: true }); }
}, catch (err) {
updated () { addNotification({ status: 'error', message: err.stack });
if (this.$refs.table) }
this.refreshScroller();
if (this.$refs.tableWrapper) isQuering.value = false;
this.scrollElement = this.$refs.tableWrapper; };
},
mounted () {
this.getProcessesList();
window.addEventListener('resize', this.resizeResults);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
async getProcessesList () {
this.isQuering = true;
// if table changes clear cached values const setRefreshInterval = () => {
if (this.lastTable !== this.table) clearRefresh();
this.results = [];
try { // Table data if (+autorefreshTimer.value) {
const { status, response } = await Schema.getProcesses(this.connection.uid); refreshInterval.value = setInterval(() => {
if (!isQuering.value)
if (status === 'success') { getProcessesList();
this.results = response; }, autorefreshTimer.value * 1000);
this.fields = response.length ? Object.keys(response[0]) : [];
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
setRefreshInterval () {
this.clearRefresh();
if (+this.autorefreshTimer) {
this.refreshInterval = setInterval(() => {
if (!this.isQuering)
this.getProcessesList();
}, this.autorefreshTimer * 1000);
}
},
clearRefresh () {
if (this.refreshInterval)
clearInterval(this.refreshInterval);
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el) {
const size = el.offsetHeight;
this.resultsSize = size;
}
this.$refs.resultTable.updateWindow();
}
},
refreshScroller () {
this.resizeResults();
},
sort (field) {
if (field === this.currentSort) {
if (this.currentSortDir === 'asc')
this.currentSortDir = 'desc';
else
this.resetSort();
}
else {
this.currentSortDir = 'asc';
this.currentSort = field;
}
},
resetSort () {
this.currentSort = '';
this.currentSortDir = 'asc';
},
stopRefresh () {
this.autorefreshTimer = 0;
this.clearRefresh();
},
selectRow (row) {
this.selectedRow = Number(row);
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.stopRefresh();
this.selectedCell = cell;
this.selectedRow = Number(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')
this.closeModal();
if (e.key === 'F5')
this.getProcessesList();
}
} }
}; };
const clearRefresh = () => {
if (refreshInterval.value)
clearInterval(refreshInterval.value);
};
const resizeResults = () => {
if (resultTable.value) {
const el = tableWrapper.value.parentElement;
if (el) {
const size = el.offsetHeight;
resultsSize.value = size;
}
resultTable.value.updateWindow();
}
};
const refreshScroller = () => resizeResults();
const sort = (field: string) => {
if (field === currentSort.value) {
if (currentSortDir.value === 'asc')
currentSortDir.value = 'desc';
else
resetSort();
}
else {
currentSortDir.value = 'asc';
currentSort.value = field;
}
};
const resetSort = () => {
currentSort.value = '';
currentSortDir.value = 'asc';
};
const stopRefresh = () => {
autorefreshTimer.value = 0;
clearRefresh();
};
const selectRow = (row: number) => {
selectedRow.value = Number(row);
};
const contextMenu = (event: MouseEvent, cell: { id: number; field: string }) => {
if ((event.target as HTMLElement).localName === 'input') return;
stopRefresh();
selectedCell.value = cell;
selectedRow.value = Number(cell.id);
contextEvent.value = event;
isContext.value = true;
};
const killProcess = async () => {
try { // Table data
const { status, response } = await Schema.killProcess({ uid: props.connection.uid, pid: selectedRow.value });
if (status === 'success')
getProcessesList();
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
};
const closeContext = () => {
isContext.value = false;
};
const copyCell = () => {
const row = results.value.find(row => Number(row.id) === selectedRow.value);
const valueToCopy = row[selectedCell.value.field];
navigator.clipboard.writeText(valueToCopy);
};
const copyRow = () => {
const row = results.value.find(row => Number(row.id) === selectedRow.value);
const rowToCopy = JSON.parse(JSON.stringify(row));
navigator.clipboard.writeText(JSON.stringify(rowToCopy));
};
const closeModal = () => emit('close');
const downloadTable = (format: 'csv' | 'json') => {
if (!sortedResults.value) return;
arrayToFile({
type: format,
content: sortedResults.value,
filename: 'processes'
});
};
const onKey = (e:KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
if (e.key === 'F5')
getProcessesList();
};
window.addEventListener('keydown', onKey, { capture: true });
onMounted(() => {
getProcessesList();
window.addEventListener('resize', resizeResults);
});
onUpdated(() => {
if (table.value)
refreshScroller();
if (tableWrapper.value)
scrollElement.value = tableWrapper.value;
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey, { capture: true });
window.removeEventListener('resize', resizeResults);
clearInterval(refreshInterval.value);
});
defineExpose({ refreshScroller });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,14 +1,14 @@
<template> <template>
<BaseContextMenu <BaseContextMenu
:context-event="contextEvent" :context-event="props.contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div v-if="selectedRow" class="context-element"> <div v-if="props.selectedRow" class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ $t('word.copy') }}</span> <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" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu"> <div class="context-submenu">
<div <div
v-if="selectedRow" v-if="props.selectedRow"
class="context-element" class="context-element"
@click="copyCell" @click="copyCell"
> >
@@ -17,7 +17,7 @@
</span> </span>
</div> </div>
<div <div
v-if="selectedRow" v-if="props.selectedRow"
class="context-element" class="context-element"
@click="copyRow" @click="copyRow"
> >
@@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<div <div
v-if="selectedRow" v-if="props.selectedRow"
class="context-element" class="context-element"
@click="killProcess" @click="killProcess"
> >
@@ -39,38 +39,33 @@
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
export default { const props = defineProps({
name: 'ModalProcessesListContext', contextEvent: MouseEvent,
components: { selectedRow: Number,
BaseContextMenu selectedCell: Object
}, });
props: {
contextEvent: MouseEvent, const emit = defineEmits(['close-context', 'copy-cell', 'copy-row', 'kill-process']);
selectedRow: Number,
selectedCell: Object const closeContext = () => {
}, emit('close-context');
emits: ['close-context', 'copy-cell', 'copy-row', 'kill-process'], };
computed: {
}, const copyCell = () => {
methods: { emit('copy-cell');
closeContext () { closeContext();
this.$emit('close-context'); };
},
copyCell () { const copyRow = () => {
this.$emit('copy-cell'); emit('copy-row');
this.closeContext(); closeContext();
}, };
copyRow () {
this.$emit('copy-row'); const killProcess = () => {
this.closeContext(); emit('kill-process');
}, closeContext();
killProcess () {
this.$emit('kill-process');
this.closeContext();
}
}
}; };
</script> </script>

View File

@@ -31,11 +31,12 @@
<div> <div>
<div> <div>
<TextEditor <TextEditor
:value="row.info || ''" :model-value="props.row.info || ''"
editor-class="textarea-editor" editor-class="textarea-editor"
:mode="editorMode" :mode="editorMode"
:read-only="true" :read-only="true"
/> />
<div class="mb-4" />
</div> </div>
</div> </div>
</template> </template>
@@ -43,60 +44,46 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import ConfirmModal from '@/components/BaseConfirmModal'; import { Ref, ref } from 'vue';
import TextEditor from '@/components/BaseTextEditor'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import TextEditor from '@/components/BaseTextEditor.vue';
export default { const props = defineProps({
name: 'ModalProcessesListRow', row: Object
components: { });
ConfirmModal,
TextEditor const emit = defineEmits(['select-row', 'contextmenu', 'stop-refresh']);
},
props: { const isInlineEditor: Ref<{[key: string]: boolean}> = ref({});
row: Object const isInfoModal = ref(false);
}, const editorMode = ref('sql');
emits: ['select-row', 'contextmenu', 'stop-refresh'],
data () { const isNull = (value: string | number) => value === null ? ' is-null' : '';
return {
isInlineEditor: {}, const selectRow = () => {
isInfoModal: false, emit('select-row');
editorMode: 'sql'
};
},
computed: {},
methods: {
isNull (value) {
return value === null ? ' is-null' : '';
},
selectRow () {
this.$emit('select-row');
},
openContext (event, payload) {
this.$emit('contextmenu', event, payload);
},
hideInfoModal () {
this.isInfoModal = false;
},
dblClick (col) {
if (col !== 'info') return;
this.$emit('stop-refresh');
this.isInfoModal = true;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
},
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
}
}
}; };
const openContext = (event: MouseEvent, payload: { id: number; field: string }) => {
emit('contextmenu', event, payload);
};
const hideInfoModal = () => {
isInfoModal.value = false;
};
const dblClick = (col: string) => {
if (col !== 'info') return;
emit('stop-refresh');
isInfoModal.value = true;
};
const cutText = (val: string | number) => {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -2,12 +2,12 @@
<Teleport to="#window-content"> <Teleport to="#window-content">
<div id="settings" class="modal active"> <div id="settings" class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" /> <a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container"> <div ref="trapRef" class="modal-container">
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-cog mr-1" /> <i class="mdi mdi-24px mdi-cog mr-1" />
<span class="cut-text">{{ $t('word.settings') }}</span> <span class="cut-text">{{ t('word.settings') }}</span>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click="closeModal" /> <a class="btn btn-clear c-hand" @click="closeModal" />
@@ -21,14 +21,14 @@
:class="{'active': selectedTab === 'general'}" :class="{'active': selectedTab === 'general'}"
@click="selectTab('general')" @click="selectTab('general')"
> >
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ t('word.general') }}</a>
</li> </li>
<li <li
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'themes'}" :class="{'active': selectedTab === 'themes'}"
@click="selectTab('themes')" @click="selectTab('themes')"
> >
<a class="tab-link">{{ $t('word.themes') }}</a> <a class="tab-link">{{ t('word.themes') }}</a>
</li> </li>
<li <li
v-if="updateStatus !== 'disabled'" v-if="updateStatus !== 'disabled'"
@@ -36,21 +36,21 @@
:class="{'active': selectedTab === 'update'}" :class="{'active': selectedTab === 'update'}"
@click="selectTab('update')" @click="selectTab('update')"
> >
<a class="tab-link" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a> <a class="tab-link" :class="{'badge badge-update': hasUpdates}">{{ t('word.update') }}</a>
</li> </li>
<li <li
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'changelog'}" :class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')" @click="selectTab('changelog')"
> >
<a class="tab-link">{{ $t('word.changelog') }}</a> <a class="tab-link">{{ t('word.changelog') }}</a>
</li> </li>
<li <li
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'about'}" :class="{'active': selectedTab === 'about'}"
@click="selectTab('about')" @click="selectTab('about')"
> >
<a class="tab-link">{{ $t('word.about') }}</a> <a class="tab-link">{{ t('word.about') }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -58,63 +58,52 @@
<div class="container"> <div class="container">
<form class="form-horizontal columns"> <form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1"> <div class="column col-12 h6 text-uppercase mb-1">
{{ $t('word.application') }} {{ t('word.application') }}
</div> </div>
<div class="column col-12 col-sm-12 mb-2 columns"> <div class="column col-12 col-sm-12 mb-2 columns">
<div class="form-group column col-12"> <div class="form-group column col-12">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" /> <i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
<select <BaseSelect
v-model="localLocale" v-model="localLocale"
class="form-select" class="form-select"
:options="locales"
option-track-by="code"
option-label="name"
@change="changeLocale(localLocale)" @change="changeLocale(localLocale)"
> />
<option
v-for="(locale, key) in locales"
:key="key"
:value="locale.code"
>
{{ locale.name }}
</option>
</select>
</div> </div>
<div class="col-4 col-sm-12 px-2 p-vcentered"> <div class="col-4 col-sm-12 px-2 p-vcentered">
<small class="d-block" style="line-height:1.1; font-size:70%;"> <small class="d-block" style="line-height: 1.1; font-size: 70%;">
{{ $t('message.missingOrIncompleteTranslation') }}<br> {{ t('message.missingOrIncompleteTranslation') }}<br>
<a class="text-bold c-hand" @click="openOutside('https://github.com/antares-sql/antares/wiki/Translate-Antares')">{{ $t('message.findOutHowToContribute') }}</a> <a class="text-bold c-hand" @click="openOutside('https://github.com/antares-sql/antares/wiki/Translate-Antares')">{{ t('message.findOutHowToContribute') }}</a>
</small> </small>
</div> </div>
</div> </div>
<div class="form-group column col-12"> <div class="form-group column col-12">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.dataTabPageSize') }} {{ t('message.dataTabPageSize') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
<select <BaseSelect
v-model="localPageSize" v-model="localPageSize"
class="form-select" class="form-select"
:options="pageSizes"
@change="changePageSize(+localPageSize)" @change="changePageSize(+localPageSize)"
> />
<option
v-for="size in pageSizes"
:key="size"
>
{{ size }}
</option>
</select>
</div> </div>
</div> </div>
<div class="form-group column col-12 mb-0"> <div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.restorePreviourSession') }} {{ t('message.restorePreviourSession') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@@ -127,7 +116,7 @@
<div class="form-group column col-12 mb-0"> <div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.disableBlur') }} {{ t('message.disableBlur') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@@ -140,7 +129,7 @@
<div class="form-group column col-12"> <div class="form-group column col-12">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.notificationsTimeout') }} {{ t('message.notificationsTimeout') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@@ -152,19 +141,19 @@
min="1" min="1"
@focusout="checkNotificationsTimeout" @focusout="checkNotificationsTimeout"
> >
<span class="input-group-addon">{{ $t('word.seconds') }}</span> <span class="input-group-addon">{{ t('word.seconds') }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="column col-12 h6 mt-4 text-uppercase mb-1"> <div class="column col-12 h6 mt-4 text-uppercase mb-1">
{{ $t('word.editor') }} {{ t('word.editor') }}
</div> </div>
<div class="column col-12 col-sm-12 columns"> <div class="column col-12 col-sm-12 columns">
<div class="form-group column col-12 mb-0"> <div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('word.autoCompletion') }} {{ t('word.autoCompletion') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@@ -177,7 +166,7 @@
<div class="form-group column col-12 mb-0"> <div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.wrapLongLines') }} {{ t('message.wrapLongLines') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@@ -196,18 +185,18 @@
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
<div class="column col-12 h6 text-uppercase mb-2"> <div class="column col-12 h6 text-uppercase mb-2">
{{ $t('message.applicationTheme') }} {{ t('message.applicationTheme') }}
</div> </div>
<div <div
class="column col-6 c-hand theme-block" class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'dark'}" :class="{'selected': applicationTheme === 'dark'}"
@click="changeApplicationTheme('dark')" @click="changeApplicationTheme('dark')"
> >
<img src="../images/dark.png" class="img-responsive img-fit-cover s-rounded"> <img :src="darkPreview" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-light"> <div class="theme-name text-light">
<i class="mdi mdi-moon-waning-crescent mdi-48px" /> <i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
{{ $t('word.dark') }} {{ t('word.dark') }}
</div> </div>
</div> </div>
</div> </div>
@@ -216,11 +205,11 @@
:class="{'selected': applicationTheme === 'light'}" :class="{'selected': applicationTheme === 'light'}"
@click="changeApplicationTheme('light')" @click="changeApplicationTheme('light')"
> >
<img src="../images/light.png" class="img-responsive img-fit-cover s-rounded"> <img :src="lightPreview" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-dark"> <div class="theme-name text-dark">
<i class="mdi mdi-white-balance-sunny mdi-48px" /> <i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
{{ $t('word.light') }} {{ t('word.light') }}
</div> </div>
</div> </div>
</div> </div>
@@ -228,29 +217,19 @@
<div class="columns mt-4"> <div class="columns mt-4">
<div class="column col-12 h6 text-uppercase mb-2 mt-4"> <div class="column col-12 h6 text-uppercase mb-2 mt-4">
{{ $t('message.editorTheme') }} {{ t('message.editorTheme') }}
</div> </div>
<div class="column col-6 h5 mb-4"> <div class="column col-6 h5 mb-4">
<select <BaseSelect
v-model="localEditorTheme" v-model="localEditorTheme"
class="form-select" class="form-select"
:options="editorThemes"
option-label="name"
option-track-by="code"
group-label="group"
group-values="themes"
@change="changeEditorTheme(localEditorTheme)" @change="changeEditorTheme(localEditorTheme)"
> />
<optgroup
v-for="group in editorThemes"
:key="group.group"
:label="group.group"
>
<option
v-for="theme in group.themes"
:key="theme.name"
:value="theme.code"
:selected="editorTheme === theme.code"
>
{{ theme.name }}
</option>
</optgroup>
</select>
</div> </div>
<div class="column col-6 mb-4"> <div class="column col-6 mb-4">
<div class="btn-group btn-group-block"> <div class="btn-group btn-group-block">
@@ -259,21 +238,21 @@
:class="{'active': editorFontSize === 'small'}" :class="{'active': editorFontSize === 'small'}"
@click="changeEditorFontSize('small')" @click="changeEditorFontSize('small')"
> >
{{ $t('word.small') }} {{ t('word.small') }}
</button> </button>
<button <button
class="btn btn-dark cut-text" class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'medium'}" :class="{'active': editorFontSize === 'medium'}"
@click="changeEditorFontSize('medium')" @click="changeEditorFontSize('medium')"
> >
{{ $t('word.medium') }} {{ t('word.medium') }}
</button> </button>
<button <button
class="btn btn-dark cut-text" class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'large'}" :class="{'active': editorFontSize === 'large'}"
@click="changeEditorFontSize('large')" @click="changeEditorFontSize('large')"
> >
{{ $t('word.large') }} {{ t('word.large') }}
</button> </button>
</div> </div>
</div> </div>
@@ -299,19 +278,19 @@
<div v-show="selectedTab === 'about'" class="panel-body py-4"> <div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center"> <div class="text-center">
<img src="../images/logo.svg" width="128"> <img :src="appLogo" width="128">
<h4>{{ appName }}</h4> <h4>{{ appName }}</h4>
<p class="mb-2"> <p class="mb-2">
{{ $t('word.version') }} {{ appVersion }}<br> {{ t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/antares-sql/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> Website</a><br> <a class="c-hand" @click="openOutside('https://github.com/antares-sql/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> Website</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">{{ appAuthor }}</a></small><br> <small>{{ t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">{{ appAuthor }}</a></small><br>
</p> </p>
<div class="mb-2"> <div class="mb-2">
<small class="d-block text-uppercase">{{ $t('word.contributors') }}:</small> <small class="d-block text-uppercase">{{ t('word.contributors') }}:</small>
<div class="d-block py-1"> <div class="d-block py-1">
<small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small> <small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small>
</div> </div>
<small>{{ $t('message.madeWithJS') }}</small> <small>{{ t('message.madeWithJS') }}</small>
</div> </div>
</div> </div>
</div> </div>
@@ -322,174 +301,124 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import { onBeforeUnmount, Ref, ref } from 'vue';
import { shell } from 'electron'; import { shell } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import localesNames from '@/i18n/supported-locales'; import { useFocusTrap } from '@/composables/useFocusTrap';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate'; import { localesNames } from '@/i18n/supported-locales';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog'; import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue';
import BaseTextEditor from '@/components/BaseTextEditor'; import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue';
import BaseTextEditor from '@/components/BaseTextEditor.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { computed } from '@vue/reactivity';
export default { const { t, availableLocales } = useI18n();
name: 'ModalSettings',
components: { const applicationStore = useApplicationStore();
ModalSettingsUpdate, const settingsStore = useSettingsStore();
ModalSettingsChangelog, const workspacesStore = useWorkspacesStore();
BaseTextEditor
const { trapRef } = useFocusTrap({ disableAutofocus: true });
const {
selectedSettingTab,
updateStatus
} = storeToRefs(applicationStore);
const {
locale: selectedLocale,
dataTabLimit: pageSize,
autoComplete: selectedAutoComplete,
lineWrap: selectedLineWrap,
notificationsTimeout,
restoreTabs,
disableBlur,
applicationTheme,
editorTheme,
editorFontSize
} = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
changeLocale,
changePageSize,
changeRestoreTabs,
changeDisableBlur,
changeAutoComplete,
changeLineWrap,
changeApplicationTheme,
changeEditorTheme,
changeEditorFontSize,
updateNotificationsTimeout
} = settingsStore;
const {
hideSettingModal: closeModal,
appName,
appVersion
} = applicationStore;
const { getWorkspace } = workspacesStore;
const appAuthor = 'Fabio Di Stasio';
const pageSizes = [30, 40, 100, 250, 500, 1000];
const contributors = process.env.APP_CONTRIBUTORS;
const appLogo = require('../images/logo.svg');
const darkPreview = require('../images/dark.png');
const lightPreview = require('../images/light.png');
const editorThemes= [
{
group: t('word.light'),
themes: [
{ code: 'chrome', name: 'Chrome' },
{ code: 'clouds', name: 'Clouds' },
{ code: 'crimson_editor', name: 'Crimson Editor' },
{ code: 'dawn', name: 'Dawn' },
{ code: 'dreamweaver', name: 'Dreamweaver' },
{ code: 'eclupse', name: 'Eclipse' },
{ code: 'github', name: 'GitHub' },
{ code: 'iplastic', name: 'IPlastic' },
{ code: 'solarized_light', name: 'Solarized Light' },
{ code: 'textmate', name: 'TextMate' },
{ code: 'tomorrow', name: 'Tomorrow' },
{ code: 'xcode', name: 'Xcode' },
{ code: 'kuroir', name: 'Kuroir' },
{ code: 'katzenmilch', name: 'KatzenMilch' },
{ code: 'sqlserver', name: 'SQL Server' }
]
}, },
setup () { {
const applicationStore = useApplicationStore(); group: t('word.dark'),
const settingsStore = useSettingsStore(); themes: [
const workspacesStore = useWorkspacesStore(); { code: 'ambiance', name: 'Ambiance' },
{ code: 'chaos', name: 'Chaos' },
const { { code: 'clouds_midnight', name: 'Clouds Midnight' },
selectedSettingTab, { code: 'dracula', name: 'Dracula' },
updateStatus { code: 'cobalt', name: 'Cobalt' },
} = storeToRefs(applicationStore); { code: 'gruvbox', name: 'Gruvbox' },
const { { code: 'gob', name: 'Green on Black' },
locale: selectedLocale, { code: 'idle_fingers', name: 'Idle Fingers' },
dataTabLimit: pageSize, { code: 'kr_theme', name: 'krTheme' },
autoComplete: selectedAutoComplete, { code: 'merbivore', name: 'Merbivore' },
lineWrap: selectedLineWrap, { code: 'mono_industrial', name: 'Mono Industrial' },
notificationsTimeout, { code: 'monokai', name: 'Monokai' },
restoreTabs, { code: 'nord_dark', name: 'Nord Dark' },
disableBlur, { code: 'pastel_on_dark', name: 'Pastel on Dark' },
applicationTheme, { code: 'solarized_dark', name: 'Solarized Dark' },
editorTheme, { code: 'terminal', name: 'Terminal' },
editorFontSize { code: 'tomorrow_night', name: 'Tomorrow Night' },
} = storeToRefs(settingsStore); { code: 'tomorrow_night_blue', name: 'Tomorrow Night Blue' },
{ code: 'tomorrow_night_bright', name: 'Tomorrow Night Bright' },
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); { code: 'tomorrow_night_eighties', name: 'Tomorrow Night 80s' },
{ code: 'twilight', name: 'Twilight' },
const { { code: 'vibrant_ink', name: 'Vibrant Ink' }
changeLocale, ]
changePageSize, }
changeRestoreTabs, ];
changeDisableBlur, const exampleQuery = `-- This is an example
changeAutoComplete,
changeLineWrap,
changeApplicationTheme,
changeEditorTheme,
changeEditorFontSize,
updateNotificationsTimeout
} = settingsStore;
const {
hideSettingModal,
appName,
appVersion
} = applicationStore;
const { getWorkspace } = workspacesStore;
return {
appName,
appVersion,
selectedSettingTab,
updateStatus,
closeModal: hideSettingModal,
selectedLocale,
pageSize,
selectedAutoComplete,
selectedLineWrap,
notificationsTimeout,
restoreTabs,
disableBlur,
applicationTheme,
editorTheme,
editorFontSize,
changeLocale,
changePageSize,
changeRestoreTabs,
changeDisableBlur,
changeAutoComplete,
changeLineWrap,
changeApplicationTheme,
changeEditorTheme,
changeEditorFontSize,
updateNotificationsTimeout,
selectedWorkspace,
getWorkspace
};
},
data () {
return {
appAuthor: 'Fabio Di Stasio',
localLocale: null,
localPageSize: null,
localTimeout: null,
localEditorTheme: null,
selectedTab: 'general',
pageSizes: [30, 40, 100, 250, 500, 1000],
editorThemes: [
{
group: this.$t('word.light'),
themes: [
{ code: 'chrome', name: 'Chrome' },
{ code: 'clouds', name: 'Clouds' },
{ code: 'crimson_editor', name: 'Crimson Editor' },
{ code: 'dawn', name: 'Dawn' },
{ code: 'dreamweaver', name: 'Dreamweaver' },
{ code: 'eclupse', name: 'Eclipse' },
{ code: 'github', name: 'GitHub' },
{ code: 'iplastic', name: 'IPlastic' },
{ code: 'solarized_light', name: 'Solarized Light' },
{ code: 'textmate', name: 'TextMate' },
{ code: 'tomorrow', name: 'Tomorrow' },
{ code: 'xcode', name: 'Xcode' },
{ code: 'kuroir', name: 'Kuroir' },
{ code: 'katzenmilch', name: 'KatzenMilch' },
{ code: 'sqlserver', name: 'SQL Server' }
]
},
{
group: this.$t('word.dark'),
themes: [
{ code: 'ambiance', name: 'Ambiance' },
{ code: 'chaos', name: 'Chaos' },
{ code: 'clouds_midnight', name: 'Clouds Midnight' },
{ code: 'dracula', name: 'Dracula' },
{ code: 'cobalt', name: 'Cobalt' },
{ code: 'gruvbox', name: 'Gruvbox' },
{ code: 'gob', name: 'Green on Black' },
{ code: 'idle_fingers', name: 'Idle Fingers' },
{ code: 'kr_theme', name: 'krTheme' },
{ code: 'merbivore', name: 'Merbivore' },
{ code: 'mono_industrial', name: 'Mono Industrial' },
{ code: 'monokai', name: 'Monokai' },
{ code: 'nord_dark', name: 'Nord Dark' },
{ code: 'pastel_on_dark', name: 'Pastel on Dark' },
{ code: 'solarized_dark', name: 'Solarized Dark' },
{ code: 'terminal', name: 'Terminal' },
{ code: 'tomorrow_night', name: 'Tomorrow Night' },
{ code: 'tomorrow_night_blue', name: 'Tomorrow Night Blue' },
{ code: 'tomorrow_night_bright', name: 'Tomorrow Night Bright' },
{ code: 'tomorrow_night_eighties', name: 'Tomorrow Night 80s' },
{ code: 'twilight', name: 'Twilight' },
{ code: 'vibrant_ink', name: 'Vibrant Ink' }
]
}
],
contributors: process.env.APP_CONTRIBUTORS
};
},
computed: {
locales () {
const locales = [];
for (const locale of this.$i18n.availableLocales)
locales.push({ code: locale, name: localesNames[locale] });
return locales;
},
hasUpdates () {
return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
},
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
exampleQuery () {
return `-- This is an example
SELECT SELECT
employee.id, employee.id,
employee.first_name, employee.first_name,
@@ -504,57 +433,81 @@ GROUP BY
ORDER BY ORDER BY
employee.id ASC; employee.id ASC;
`; `;
},
otherContributors () {
return this.contributors
.split(',')
.filter(c => !c.includes(this.appAuthor))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
}
},
created () {
this.localLocale = this.selectedLocale;
this.localPageSize = this.pageSize;
this.localTimeout = this.notificationsTimeout;
this.localEditorTheme = this.editorTheme;
this.selectedTab = this.selectedSettingTab;
window.addEventListener('keydown', this.onKey);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
selectTab (tab) {
this.selectedTab = tab;
},
openOutside (link) {
shell.openExternal(link);
},
checkNotificationsTimeout () {
if (!this.localTimeout)
this.localTimeout = 10;
this.updateNotificationsTimeout(+this.localTimeout); const localLocale: Ref<string> = ref(null);
}, const localPageSize: Ref<number> = ref(null);
onKey (e) { const localTimeout: Ref<number> = ref(null);
e.stopPropagation(); const localEditorTheme: Ref<string> = ref(null);
if (e.key === 'Escape') const selectedTab: Ref<string> = ref('general');
this.closeModal();
}, const locales = computed(() => {
toggleRestoreSession () { const locales = [];
this.changeRestoreTabs(!this.restoreTabs); for (const locale of availableLocales)
}, locales.push({ code: locale, name: localesNames[locale] });
toggleDisableBlur () {
this.changeDisableBlur(!this.disableBlur); return locales;
}, });
toggleAutoComplete () {
this.changeAutoComplete(!this.selectedAutoComplete); const hasUpdates = computed(() => ['available', 'downloading', 'downloaded', 'link'].includes(updateStatus.value));
},
toggleLineWrap () { const workspace = computed(() => {
this.changeLineWrap(!this.selectedLineWrap); return getWorkspace(selectedWorkspace.value);
} });
}
const otherContributors = computed(() => {
return contributors
.split(',')
.filter(c => !c.includes(appAuthor))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
});
const selectTab = (tab: string) => {
selectedTab.value = tab;
}; };
const openOutside = (link: string) => {
shell.openExternal(link);
};
const checkNotificationsTimeout = () => {
if (!localTimeout.value)
localTimeout.value = 10;
updateNotificationsTimeout(+localTimeout.value);
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
const toggleRestoreSession = () => {
changeRestoreTabs(!restoreTabs.value);
};
const toggleDisableBlur = () => {
changeDisableBlur(!disableBlur.value);
};
const toggleAutoComplete = () => {
changeAutoComplete(!selectedAutoComplete.value);
};
const toggleLineWrap = () => {
changeLineWrap(!selectedLineWrap.value);
};
localLocale.value = selectedLocale.value as string;
localPageSize.value = pageSize.value as number;
localTimeout.value = notificationsTimeout.value as number;
localEditorTheme.value = editorTheme.value as string;
selectedTab.value = selectedSettingTab.value;
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -13,66 +13,53 @@
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts">
<script>
import { marked } from 'marked'; import { marked } from 'marked';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { ref } from 'vue';
export default { const { appVersion } = useApplicationStore();
name: 'ModalSettingsChangelog',
components: { const changelog = ref('');
BaseLoader const isLoading = ref(true);
}, const error = ref('');
setup () { const isError = ref(false);
const { appVersion } = useApplicationStore();
return { appVersion }; const getChangelog = async () => {
}, try {
data () { const apiRes = await fetch(`https://api.github.com/repos/antares-sql/antares/releases/tags/v${appVersion}`, {
return { method: 'GET'
changelog: '', });
isLoading: true,
error: '', const { body } = await apiRes.json();
isError: false const cutOffset = body.indexOf('### Download');
const markdown = cutOffset >= 0
? body.substr(0, cutOffset)
: body;
const renderer = {
link (href: string, title: string, text: string) {
return text;
},
listitem (text: string) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
}
}; };
},
created () {
this.getChangelog();
},
methods: {
async getChangelog () {
try {
const apiRes = await fetch(`https://api.github.com/repos/antares-sql/antares/releases/tags/v${this.appVersion}`, {
method: 'GET'
});
const { body } = await apiRes.json(); marked.use({ renderer });
const cutOffset = body.indexOf('### Download');
const markdown = cutOffset >= 0
? body.substr(0, cutOffset)
: body;
const renderer = { changelog.value = marked(markdown);
link (href, title, text) {
return text;
},
listitem (text) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
}
};
marked.use({ renderer });
this.changelog = marked(markdown);
}
catch (err) {
this.error = err.message;
this.isError = true;
}
this.isLoading = false;
}
} }
catch (err) {
error.value = err.message;
isError.value = true;
}
isLoading.value = false;
}; };
getChangelog();
</script> </script>
<style lang="scss"> <style lang="scss">
#changelog { #changelog {

View File

@@ -52,68 +52,61 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { ipcRenderer, shell } from 'electron'; import { ipcRenderer, shell } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
export default { const { t } = useI18n();
name: 'ModalSettingsUpdate',
setup () {
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const { const applicationStore = useApplicationStore();
updateStatus, const settingsStore = useSettingsStore();
getDownloadProgress
} = storeToRefs(applicationStore);
const { allowPrerelease } = storeToRefs(settingsStore);
const { changeAllowPrerelease } = settingsStore; const {
updateStatus,
getDownloadProgress: downloadPercentage
} = storeToRefs(applicationStore);
const { allowPrerelease } = storeToRefs(settingsStore);
return { const { changeAllowPrerelease } = settingsStore;
updateStatus,
downloadPercentage: getDownloadProgress, const updateMessage = computed(() => {
allowPrerelease, switch (updateStatus.value) {
changeAllowPrerelease case 'noupdate':
}; return t('message.noUpdatesAvailable');
}, case 'checking':
computed: { return t('message.checkingForUpdate');
updateMessage () { case 'nocheck':
switch (this.updateStatus) { return t('message.checkFailure');
case 'noupdate': case 'available':
return this.$t('message.noUpdatesAvailable'); return t('message.updateAvailable');
case 'checking': case 'downloading':
return this.$t('message.checkingForUpdate'); return t('message.downloadingUpdate');
case 'nocheck': case 'downloaded':
return this.$t('message.checkFailure'); return t('message.updateDownloaded');
case 'available': case 'link':
return this.$t('message.updateAvailable'); return t('message.updateAvailable');
case 'downloading': default:
return this.$t('message.downloadingUpdate'); return updateStatus.value;
case 'downloaded':
return this.$t('message.updateDownloaded');
case 'link':
return this.$t('message.updateAvailable');
default:
return this.updateStatus;
}
}
},
methods: {
openOutside (link) {
shell.openExternal(link);
},
checkForUpdates () {
ipcRenderer.send('check-for-updates');
},
restartToUpdate () {
ipcRenderer.send('restart-to-update');
},
toggleAllowPrerelease () {
this.changeAllowPrerelease(!this.allowPrerelease);
}
} }
});
const openOutside = (link: string) => {
shell.openExternal(link);
};
const checkForUpdates = () => {
ipcRenderer.send('check-for-updates');
};
const restartToUpdate = () => {
ipcRenderer.send('restart-to-update');
};
const toggleAllowPrerelease = () => {
changeAllowPrerelease(!allowPrerelease.value);
}; };
</script> </script>

View File

@@ -8,7 +8,8 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, onMounted, Prop, Ref, ref, toRef, watch } from 'vue';
import * as ace from 'ace-builds'; import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver'; import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools'; import '../libs/ext-language_tools';
@@ -16,329 +17,330 @@ import { storeToRefs } from 'pinia';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { Workspace } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
export default { const editor: Ref<ace.Ace.Editor> = ref(null);
name: 'QueryEditor', const applicationStore = useApplicationStore();
props: { const settingsStore = useSettingsStore();
modelValue: String,
workspace: Object,
isSelected: Boolean,
schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
height: { type: Number, default: 200 }
},
emits: ['update:modelValue'],
setup () {
const editor = null;
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const { setBaseCompleters } = applicationStore; const { setBaseCompleters } = applicationStore;
const { baseCompleter } = storeToRefs(applicationStore); const { baseCompleter } = storeToRefs(applicationStore);
const { const {
editorTheme, editorTheme,
editorFontSize, editorFontSize,
autoComplete, autoComplete,
lineWrap lineWrap
} = storeToRefs(settingsStore); } = storeToRefs(settingsStore);
return { const props = defineProps({
editor, modelValue: String,
baseCompleter, workspace: Object as Prop<Workspace>,
setBaseCompleters, isSelected: Boolean,
editorTheme, schema: { type: String, default: '' },
editorFontSize, autoFocus: { type: Boolean, default: false },
autoComplete, readOnly: { type: Boolean, default: false },
lineWrap height: { type: Number, default: 200 }
}; });
},
data () { const emit = defineEmits(['update:modelValue']);
return {
cursorPosition: 0, const cursorPosition = ref(0);
fields: [], const fields = ref([]);
customCompleter: [], const customCompleter = ref([]);
id: uidGen(), const id = ref(uidGen());
lastSchema: null const lastSchema: Ref<string> = ref(null);
};
}, const tables = computed(() => {
computed: { return props.workspace
tables () { ? props.workspace.structure.filter(schema => schema.name === props.schema)
return this.workspace .reduce((acc, curr) => {
? this.workspace.structure.filter(schema => schema.name === this.schema) acc.push(...curr.tables);
.reduce((acc, curr) => { return acc;
acc.push(...curr.tables); }, []).map(table => {
return acc; return {
}, []).map(table => { name: table.name as string,
return { type: table.type as string,
name: table.name, fields: []
type: table.type, };
fields: [] })
}; : [];
}) });
: [];
}, const triggers = computed(() => {
triggers () { return props.workspace
return this.workspace ? props.workspace.structure.filter(schema => schema.name === props.schema)
? this.workspace.structure.filter(schema => schema.name === this.schema) .reduce((acc, curr) => {
.reduce((acc, curr) => { acc.push(...curr.triggers);
acc.push(...curr.triggers); return acc;
return acc; }, []).map(trigger => {
}, []).map(trigger => { return {
return { name: trigger.name as string,
name: trigger.name, type: 'trigger'
type: 'trigger' };
}; })
}) : [];
: []; });
},
procedures () { const procedures = computed(() => {
return this.workspace return props.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema) ? props.workspace.structure.filter(schema => schema.name === props.schema)
.reduce((acc, curr) => { .reduce((acc, curr) => {
acc.push(...curr.procedures); acc.push(...curr.procedures);
return acc; return acc;
}, []).map(procedure => { }, []).map(procedure => {
return { return {
name: `${procedure.name}()`, name: `${procedure.name}()`,
type: 'routine' type: 'routine'
}; };
}) })
: []; : [];
}, });
functions () {
return this.workspace const functions = computed(() => {
? this.workspace.structure.filter(schema => schema.name === this.schema) return props.workspace
.reduce((acc, curr) => { ? props.workspace.structure.filter(schema => schema.name === props.schema)
acc.push(...curr.functions); .reduce((acc, curr) => {
return acc; acc.push(...curr.functions);
}, []).map(func => { return acc;
return { }, []).map(func => {
name: `${func.name}()`, return {
type: 'function' name: `${func.name}()`,
}; type: 'function'
}) };
: []; })
}, : [];
schedulers () { });
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema) const schedulers = computed(() => {
.reduce((acc, curr) => { return props.workspace
acc.push(...curr.schedulers); ? props.workspace.structure.filter(schema => schema.name === props.schema)
return acc; .reduce((acc, curr) => {
}, []).map(scheduler => { acc.push(...curr.schedulers);
return { return acc;
name: scheduler.name, }, []).map(scheduler => {
type: 'scheduler' return {
}; name: scheduler.name as string,
}) type: 'scheduler'
: []; };
}, })
mode () { : [];
switch (this.workspace.client) { });
case 'mysql':
case 'maria': const mode = computed(() => {
return 'mysql'; switch (props.workspace.client) {
case 'mssql': case 'mysql':
return 'sqlserver'; case 'maria':
case 'pg': return 'mysql';
return 'pgsql'; // case 'mssql':
default: // return 'sqlserver';
return 'sql'; case 'pg':
} return 'pgsql';
}, default:
lastWord () { return 'sql';
const charsBefore = this.modelValue.slice(0, this.cursorPosition); }
const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean); });
return words.pop();
}, const lastWord = computed(() => {
isLastWordATable () { const charsBefore = props.modelValue.slice(0, cursorPosition.value);
return /\w+\.\w*/gm.test(this.lastWord); const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean);
}, return words.pop();
fieldsCompleter () { });
return {
getCompletions: (editor, session, pos, prefix, callback) => { const isLastWordATable = computed(() => /\w+\.\w*/gm.test(lastWord.value));
const completions = [];
this.fields.forEach(field => { const fieldsCompleter = computed(() => {
completions.push({ return {
value: field, getCompletions: (editor: never, session: never, pos: never, prefix: never, callback: (err: null, response: ace.Ace.Completion[]) => void) => {
meta: 'column', const completions: ace.Ace.Completion[] = [];
score: 1000 fields.value.forEach(field => {
}); completions.push({
}); value: field,
callback(null, completions); meta: 'column',
} score: 1000
}; });
});
callback(null, completions);
} }
}, };
watch: { });
modelValue () {
this.cursorPosition = this.editor.session.doc.positionToIndex(this.editor.getCursorPosition());
},
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
editorFontSize () {
const sizes = {
small: '12px',
medium: '14px',
large: '16px'
};
if (this.editor) { const setCustomCompleter = () => {
this.editor.setOptions({ editor.value.completers.push({
fontSize: sizes[this.editorFontSize] getCompletions: (editor, session, pos, prefix, callback: (err: null, response: ace.Ace.Completion[]) => void) => {
const completions: ace.Ace.Completion[] = [];
[
...tables.value,
...triggers.value,
...procedures.value,
...functions.value,
...schedulers.value
].forEach(el => {
completions.push({
value: el.name,
meta: el.type,
score: 1000
}); });
} });
}, callback(null, completions);
autoComplete () {
if (this.editor) {
this.editor.setOptions({
enableLiveAutocompletion: this.autoComplete
});
}
},
lineWrap () {
if (this.editor) {
this.editor.setOptions({
wrap: this.lineWrap
});
}
},
isSelected () {
if (this.isSelected) {
this.lastSchema = this.schema;
this.editor.resize();
}
},
height () {
setTimeout(() => {
this.editor.resize();
}, 20);
},
lastSchema () {
if (this.editor) {
this.editor.completers = this.baseCompleter.map(el => Object.assign({}, el));
this.setCustomCompleter();
}
} }
}, });
created () {
this.lastSchema = this.schema; customCompleter.value = editor.value.completers;
}, };
mounted () {
this.editor = ace.edit(`editor-${this.id}`, { watch(() => props.modelValue, () => {
mode: `ace/mode/${this.mode}`, // eslint-disable-next-line @typescript-eslint/no-explicit-any
theme: `ace/theme/${this.editorTheme}`, cursorPosition.value = (editor.value.session as any).doc.positionToIndex(editor.value.getCursorPosition());
value: this.modelValue, });
fontSize: '14px',
printMargin: false, watch(editorTheme, () => {
readOnly: this.readOnly if (editor.value)
editor.value.setTheme(`ace/theme/${editorTheme.value}`);
});
watch(editorFontSize, () => {
const sizes = {
small: '12px',
medium: '14px',
large: '16px'
};
if (editor.value) {
editor.value.setOptions({
fontSize: sizes[editorFontSize.value]
}); });
}
});
this.editor.setOptions({ watch(autoComplete, () => {
enableBasicAutocompletion: true, if (editor.value) {
wrap: this.lineWrap, editor.value.setOptions({
enableSnippets: true, enableLiveAutocompletion: autoComplete.value
enableLiveAutocompletion: this.autoComplete
}); });
}
});
if (!this.baseCompleter.length) watch(lineWrap, () => {
this.setBaseCompleters(this.editor.completers.map(el => Object.assign({}, el))); if (editor.value) {
editor.value.setOptions({
wrap: lineWrap.value
});
}
});
this.setCustomCompleter(); watch(() => props.isSelected, () => {
if (props.isSelected) {
lastSchema.value = props.schema;
editor.value.resize();
}
});
this.editor.commands.on('afterExec', e => { watch(() => props.height, () => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) { setTimeout(() => {
if (this.isLastWordATable || e.args === '.') { editor.value.resize();
if (e.args !== ' ') { }, 20);
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop().trim()); });
if (table) { watch(lastSchema, () => {
const params = { if (editor.value) {
uid: this.workspace.uid, editor.value.completers = baseCompleter.value.map(el => Object.assign({}, el));
schema: this.schema, setCustomCompleter();
table: table.name }
}; });
Tables.getTableColumns(params).then(res => { lastSchema.value = toRef(props, 'schema').value;
if (res.response.length)
this.fields = res.response.map(field => field.name); onMounted(() => {
this.editor.completers = [this.fieldsCompleter]; editor.value = ace.edit(`editor-${id.value}`, {
this.editor.execCommand('startAutocomplete'); mode: `ace/mode/${mode.value}`,
}).catch(console.log); theme: `ace/theme/${editorTheme.value}`,
} value: props.modelValue,
else fontSize: 14,
this.editor.completers = this.customCompleter; printMargin: false,
readOnly: props.readOnly
});
editor.value.setOptions({
enableBasicAutocompletion: true,
wrap: lineWrap.value,
enableSnippets: true,
enableLiveAutocompletion: autoComplete.value
});
if (!baseCompleter.value.length)
setBaseCompleters(editor.value.completers.map(el => Object.assign({}, el)));
setCustomCompleter();
editor.value.commands.on('afterExec', (e: { args: string; command: { name: string } }) => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
if (isLastWordATable.value || e.args === '.') {
if (e.args !== ' ') {
const table = tables.value.find(t => t.name === lastWord.value.split('.').pop().trim());
if (table) {
const params = {
uid: props.workspace.uid,
schema: props.schema,
table: table.name
};
Tables.getTableColumns(params).then(res => {
if (res.response.length)
fields.value = res.response.map((field: { name: string }) => field.name);
editor.value.completers = [fieldsCompleter.value];
editor.value.execCommand('startAutocomplete');
}).catch(console.log);
} }
else else
this.editor.completers = this.customCompleter; editor.value.completers = customCompleter.value;
} }
else else
this.editor.completers = this.customCompleter; editor.value.completers = customCompleter.value;
} }
});
this.editor.session.on('change', () => {
const content = this.editor.getValue();
this.$emit('update:modelValue', content);
});
this.editor.on('guttermousedown', e => {
const target = e.domEvent.target;
if (target.className.indexOf('ace_gutter-cell') === -1)
return;
if (e.clientX > 25 + target.getBoundingClientRect().left)
return;
const row = e.getDocumentPosition().row;
const breakpoints = e.editor.session.getBreakpoints(row, 0);
if (typeof breakpoints[row] === typeof undefined)
e.editor.session.setBreakpoint(row);
else else
e.editor.session.clearBreakpoint(row); editor.value.completers = customCompleter.value;
e.stop();
});
if (this.autoFocus) {
setTimeout(() => {
this.editor.focus();
this.editor.resize();
}, 20);
} }
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(editor.value.session as any).on('change', () => {
const content = editor.value.getValue();
emit('update:modelValue', content);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(editor.value as any).on('guttermousedown', (e: any) => {
const target = e.domEvent.target;
if (target.className.indexOf('ace_gutter-cell') === -1)
return;
if (e.clientX > 25 + target.getBoundingClientRect().left)
return;
const row = e.getDocumentPosition().row;
const breakpoints = e.editor.session.getBreakpoints(row, 0);
if (typeof breakpoints[row] === typeof undefined)
e.editor.session.setBreakpoint(row);
else
e.editor.session.clearBreakpoint(row);
e.stop();
});
if (props.autoFocus) {
setTimeout(() => { setTimeout(() => {
this.editor.resize(); editor.value.focus();
editor.value.resize();
}, 20); }, 20);
},
methods: {
setCustomCompleter () {
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
this.customCompleter = this.editor.completers;
}
} }
};
setTimeout(() => {
editor.value.resize();
}, 20);
});
defineExpose({ editor });
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -3,6 +3,13 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="$emit('close-context')" @close-context="$emit('close-context')"
> >
<div
v-if="isConnected"
class="context-element"
@click="disconnect"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-power text-light pr-1" /> {{ $t('word.disconnect') }}</span>
</div>
<div class="context-element" @click="duplicateConnection"> <div class="context-element" @click="duplicateConnection">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ $t('word.duplicate') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ $t('word.duplicate') }}</span>
</div> </div>
@@ -29,83 +36,76 @@
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import { computed, Prop, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { storeToRefs } from 'pinia'; import { ConnectionParams } from 'common/interfaces/antares';
export default { const {
name: 'SettingBarContext', getConnectionName,
components: { addConnection,
BaseContextMenu, deleteConnection
ConfirmModal } = useConnectionsStore();
}, const workspacesStore = useWorkspacesStore();
props: { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
contextEvent: MouseEvent,
contextConnection: Object
},
emits: ['close-context'],
setup () {
const {
getConnectionName,
addConnection,
deleteConnection
} = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { selectWorkspace } = workspacesStore; const {
selectWorkspace,
removeConnected: disconnectWorkspace,
getWorkspace
} = workspacesStore;
return { const props = defineProps({
getConnectionName, contextEvent: MouseEvent,
addConnection, contextConnection: Object as Prop<ConnectionParams>
deleteConnection, });
selectedWorkspace,
selectWorkspace
};
},
data () {
return {
isConfirmModal: false,
isEditModal: false
};
},
computed: {
connectionName () {
return this.getConnectionName(this.contextConnection.uid);
}
},
methods: {
confirmDeleteConnection () {
if (this.selectedWorkspace === this.contextConnection.uid)
this.selectWorkspace();
this.deleteConnection(this.contextConnection);
this.closeContext();
},
duplicateConnection () {
let connectionCopy = Object.assign({}, this.contextConnection);
connectionCopy = {
...connectionCopy,
uid: uidGen('C'),
name: connectionCopy.name ? `${connectionCopy?.name}_copy` : ''
};
this.addConnection(connectionCopy); const emit = defineEmits(['close-context']);
this.closeContext();
}, const isConfirmModal = ref(false);
showConfirmModal () {
this.isConfirmModal = true; const connectionName = computed(() => getConnectionName(props.contextConnection.uid));
}, const isConnected = computed(() => getWorkspace(props.contextConnection.uid).connectionStatus === 'connected');
hideConfirmModal () {
this.isConfirmModal = false; const confirmDeleteConnection = () => {
this.closeContext(); if (selectedWorkspace.value === props.contextConnection.uid)
}, selectWorkspace(null);
closeContext () { deleteConnection(props.contextConnection);
this.$emit('close-context'); closeContext();
} };
}
const duplicateConnection = () => {
let connectionCopy = Object.assign({}, props.contextConnection);
connectionCopy = {
...connectionCopy,
uid: uidGen('C'),
name: connectionCopy.name ? `${connectionCopy?.name}_copy` : ''
};
addConnection(connectionCopy);
closeContext();
};
const showConfirmModal = () => {
isConfirmModal.value = true;
};
const hideConfirmModal = () => {
isConfirmModal.value = false;
closeContext();
};
const disconnect = () => {
disconnectWorkspace(props.contextConnection.uid);
closeContext();
};
const closeContext = () => {
emit('close-context');
}; };
</script> </script>

View File

@@ -26,46 +26,39 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { shell } from 'electron';
import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia'; import { computed, ComputedRef } from 'vue';
const { shell } = require('electron');
export default { interface DatabaseInfos {// TODO: temp
name: 'TheFooter', name: string;
setup () { number: string;
const applicationStore = useApplicationStore(); arch: string;
const workspacesStore = useWorkspacesStore(); os: string;
}
const { getSelected: workspace } = storeToRefs(workspacesStore); const applicationStore = useApplicationStore();
const workspacesStore = useWorkspacesStore();
const { appVersion, showSettingModal } = applicationStore; const { getSelected: workspace } = storeToRefs(workspacesStore);
const { getWorkspace } = workspacesStore;
return { const { showSettingModal } = applicationStore;
appVersion, const { getWorkspace } = workspacesStore;
showSettingModal,
workspace, const version: ComputedRef<DatabaseInfos> = computed(() => {
getWorkspace return getWorkspace(workspace.value) ? getWorkspace(workspace.value).version : null;
}; });
},
computed: { const versionString = computed(() => {
version () { if (version.value)
return this.getWorkspace(this.workspace) ? this.getWorkspace(this.workspace).version : null; return `${version.value.name} ${version.value.number} (${version.value.arch} ${version.value.os})`;
}, return '';
versionString () { });
if (this.version)
return `${this.version.name} ${this.version.number} (${this.version.arch} ${this.version.os})`; const openOutside = (link: string) => shell.openExternal(link);
return '';
}
},
methods: {
openOutside (link) {
shell.openExternal(link);
}
}
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -16,71 +16,51 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import BaseNotification from '@/components/BaseNotification'; import BaseNotification from '@/components/BaseNotification.vue';
import { storeToRefs } from 'pinia';
export default { const notificationsStore = useNotificationsStore();
name: 'TheNotificationsBoard', const settingsStore = useSettingsStore();
components: {
BaseNotification
},
setup () {
const notificationsStore = useNotificationsStore();
const settingsStore = useSettingsStore();
const { removeNotification } = notificationsStore; const { removeNotification } = notificationsStore;
const { notifications } = storeToRefs(notificationsStore); const { notifications } = storeToRefs(notificationsStore);
const { notificationsTimeout } = storeToRefs(settingsStore); const { notificationsTimeout } = storeToRefs(settingsStore);
return { const timeouts: Ref<{[key: string]: NodeJS.Timeout}> = ref({});
removeNotification,
notifications, const latestNotifications = computed(() => notifications.value.slice(0, 10));
notificationsTimeout
}; watch(() => notifications.value.length, (val) => {
}, if (val > 0) {
data () { const nUid: string = notifications.value[0].uid;
return { timeouts.value[nUid] = setTimeout(() => {
timeouts: {} removeNotification(nUid);
}; delete timeouts.value[nUid];
}, }, notificationsTimeout.value * 1000);
computed: { }
latestNotifications () { });
return this.notifications.slice(0, 10);
} const clearTimeouts = () => {
}, for (const uid in timeouts.value) {
watch: { clearTimeout(timeouts.value[uid]);
'notifications.length': function (val) { delete timeouts.value[uid];
if (val > 0) { }
const nUid = this.notifications[0].uid; };
this.timeouts[nUid] = setTimeout(() => {
this.removeNotification(nUid); const rearmTimeouts = () => {
delete this.timeouts[nUid]; const delay = 50;
}, this.notificationsTimeout * 1000); let i = notifications.value.length * delay;
} for (const notification of notifications.value) {
} timeouts.value[notification.uid] = setTimeout(() => {
}, removeNotification(notification.uid);
methods: { delete timeouts.value[notification.uid];
clearTimeouts () { }, (notificationsTimeout.value * 1000) + i);
for (const uid in this.timeouts) { i = i > delay ? i - delay : 0;
clearTimeout(this.timeouts[uid]);
delete this.timeouts[uid];
}
},
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) + i);
i = i > delay ? i - delay : 0;
}
}
} }
}; };
</script> </script>

View File

@@ -28,55 +28,30 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { ref, Ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import { useScratchpadStore } from '@/stores/scratchpad'; import { useScratchpadStore } from '@/stores/scratchpad';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import TextEditor from '@/components/BaseTextEditor.vue';
export default { const applicationStore = useApplicationStore();
name: 'TheScratchpad', const scratchpadStore = useScratchpadStore();
components: {
ConfirmModal,
TextEditor
},
emits: ['hide'],
setup () {
const applicationStore = useApplicationStore();
const scratchpadStore = useScratchpadStore();
const { notes } = storeToRefs(scratchpadStore); const { notes } = storeToRefs(scratchpadStore);
const { changeNotes } = scratchpadStore; const { changeNotes } = scratchpadStore;
const { hideScratchpad } = applicationStore;
return { const localNotes = ref(notes.value);
notes, const debounceTimeout: Ref<NodeJS.Timeout> = ref(null);
hideScratchpad: applicationStore.hideScratchpad,
changeNotes watch(localNotes, () => {
}; clearTimeout(debounceTimeout.value);
},
data () { debounceTimeout.value = setTimeout(() => {
return { changeNotes(localNotes.value);
localNotes: '', }, 200);
debounceTimeout: null });
};
},
watch: {
localNotes () {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => {
this.changeNotes(this.localNotes);
}, 200);
}
},
created () {
this.localNotes = this.notes;
},
methods: {
hideModal () {
this.$emit('hide');
}
}
};
</script> </script>

View File

@@ -55,108 +55,84 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { ref, Ref, computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import Draggable from 'vuedraggable'; import * as Draggable from 'vuedraggable';
import SettingBarContext from '@/components/SettingBarContext'; import SettingBarContext from '@/components/SettingBarContext.vue';
import { ConnectionParams } from 'common/interfaces/antares';
export default { const applicationStore = useApplicationStore();
name: 'TheSettingBar', const connectionsStore = useConnectionsStore();
components: { const workspacesStore = useWorkspacesStore();
Draggable,
SettingBarContext const { updateStatus } = storeToRefs(applicationStore);
const { connections: getConnections } = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { showSettingModal, showScratchpad } = applicationStore;
const { getConnectionName, updateConnections } = connectionsStore;
const { getWorkspace, selectWorkspace } = workspacesStore;
const isLinux = process.platform === 'linux';
const isContext: Ref<boolean> = ref(false);
const isDragging: Ref<boolean> = ref(false);
const contextEvent: Ref<MouseEvent> = ref(null);
const contextConnection: Ref<ConnectionParams> = ref(null);
const connections = computed({
get () {
return getConnections.value;
}, },
setup () { set (value: ConnectionParams[]) {
const applicationStore = useApplicationStore(); updateConnections(value);
const connectionsStore = useConnectionsStore(); }
const workspacesStore = useWorkspacesStore(); });
const { updateStatus } = storeToRefs(applicationStore); const hasUpdates = computed(() => ['available', 'downloading', 'downloaded', 'link'].includes(updateStatus.value));
const { connections: getConnections } = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { showSettingModal, showScratchpad } = applicationStore; const contextMenu = (event: MouseEvent, connection: ConnectionParams) => {
const { getConnectionName, updateConnections } = connectionsStore; contextEvent.value = event;
const { getWorkspace, selectWorkspace } = workspacesStore; contextConnection.value = connection;
isContext.value = true;
};
return { const tooltipPosition = (e: Event) => {
applicationStore, const el = e.target ? e.target : e;
updateStatus, const fromTop = isLinux
showSettingModal, ? window.scrollY + (el as HTMLElement).getBoundingClientRect().top + ((el as HTMLElement).offsetHeight / 4)
showScratchpad, : window.scrollY + (el as HTMLElement).getBoundingClientRect().top - ((el as HTMLElement).offsetHeight / 4);
getConnections, (el as HTMLElement).querySelector<HTMLElement>('.ex-tooltip-content').style.top = `${fromTop}px`;
getConnectionName, };
updateConnections,
selectedWorkspace,
getWorkspace,
selectWorkspace
};
},
data () {
return {
dragElement: null,
isContext: false,
isDragging: false,
contextEvent: null,
contextConnection: {},
scale: 0
};
},
computed: {
connections: {
get () {
return this.getConnections;
},
set (value) {
this.updateConnections(value);
}
},
hasUpdates () {
return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
}
},
methods: {
contextMenu (event, connection) {
this.contextEvent = event;
this.contextConnection = connection;
this.isContext = true;
},
workspaceName (connection) {
return connection.ask ? '' : `${connection.user + '@'}${connection.host}:${connection.port}`;
},
tooltipPosition (e) {
const el = e.target ? e.target : e;
const fromTop = window.pageYOffset + el.getBoundingClientRect().top - (el.offsetHeight / 4);
el.querySelector('.ex-tooltip-content').style.top = `${fromTop}px`;
},
getStatusBadge (uid) {
if (this.getWorkspace(uid)) {
const status = this.getWorkspace(uid).connectionStatus;
switch (status) { const getStatusBadge = (uid: string) => {
case 'connected': if (getWorkspace(uid)) {
return 'badge badge-connected'; const status = getWorkspace(uid).connectionStatus;
case 'connecting':
return 'badge badge-connecting';
case 'failed':
return 'badge badge-failed';
default:
return '';
}
}
},
dragStop (e) {
this.isDragging = false;
setTimeout(() => { switch (status) {
this.tooltipPosition(e.originalEvent.target.parentNode); case 'connected':
}, 200); return 'badge badge-connected';
case 'connecting':
return 'badge badge-connecting';
case 'failed':
return 'badge badge-failed';
default:
return '';
} }
} }
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dragStop = (e: any) => { // TODO: temp
isDragging.value = false;
setTimeout(() => {
tooltipPosition(e.originalEvent.target.parentNode);
}, 200);
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -1,11 +1,15 @@
<template> <template>
<div id="titlebar" @dblclick="toggleFullScreen"> <div
v-if="!isLinux"
id="titlebar"
@dblclick="toggleFullScreen"
>
<div class="titlebar-resizer" /> <div class="titlebar-resizer" />
<div class="titlebar-elements"> <div class="titlebar-elements">
<img <img
v-if="!isMacOS" v-if="!isMacOS"
class="titlebar-logo" class="titlebar-logo"
src="@/images/logo.svg" :src="appIcon"
> >
</div> </div>
<div class="titlebar-elements titlebar-title"> <div class="titlebar-elements titlebar-title">
@@ -26,105 +30,76 @@
> >
<i class="mdi mdi-24px mdi-refresh" /> <i class="mdi mdi-24px mdi-refresh" />
</div> </div>
<div <div v-if="isWindows" style="width: 140px;" />
v-if="!isMacOS"
class="titlebar-element"
@click="minimizeApp"
>
<i class="mdi mdi-24px mdi-minus" />
</div>
<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
v-if="!isMacOS"
class="titlebar-element close-button"
@click="closeApp"
>
<i class="mdi mdi-24px mdi-close" />
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { ipcRenderer } from 'electron'; import { computed, onUnmounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentWindow } from '@electron/remote'; import { getCurrentWindow } from '@electron/remote';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n';
import { ipcRenderer } from 'electron';
export default { const { t } = useI18n();
name: 'TheTitleBar',
setup () {
const { getConnectionName } = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getConnectionName } = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { getWorkspace } = workspacesStore; const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
return { const { getWorkspace } = workspacesStore;
getConnectionName,
selectedWorkspace,
getWorkspace
};
},
data () {
return {
w: getCurrentWindow(),
isMaximized: getCurrentWindow().isMaximized(),
isDevelopment: process.env.NODE_ENV === 'development',
isMacOS: process.platform === 'darwin'
};
},
computed: {
windowTitle () {
if (!this.selectedWorkspace) return '';
if (this.selectedWorkspace === 'NEW') return this.$t('message.createNewConnection');
const connectionName = this.getConnectionName(this.selectedWorkspace); const appIcon = require('@/images/logo.svg');
const workspace = this.getWorkspace(this.selectedWorkspace); const w = ref(getCurrentWindow());
const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb) || [workspace.client]; const isMaximized = ref(getCurrentWindow().isMaximized());
const isDevelopment = ref(process.env.NODE_ENV === 'development');
const isMacOS = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
const isLinux = process.platform === 'linux';
return [connectionName, ...breadcrumbs].join(' • '); const windowTitle = computed(() => {
} if (!selectedWorkspace.value) return '';
}, if (selectedWorkspace.value === 'NEW') return t('message.createNewConnection');
created () {
window.addEventListener('resize', this.onResize); const connectionName = getConnectionName(selectedWorkspace.value);
}, const workspace = getWorkspace(selectedWorkspace.value);
unmounted () { const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb) || [workspace.client];
window.removeEventListener('resize', this.onResize);
}, return [connectionName, ...breadcrumbs].join(' • ');
methods: { });
closeApp () {
ipcRenderer.send('close-app'); const toggleFullScreen = () => {
}, if (isMaximized.value)
minimizeApp () { w.value.unmaximize();
this.w.minimize(); else
}, w.value.maximize();
toggleFullScreen () {
if (this.isMaximized)
this.w.unmaximize();
else
this.w.maximize();
},
openDevTools () {
this.w.openDevTools();
},
reload () {
this.w.reload();
},
onResize () {
this.isMaximized = this.w.isMaximized();
}
}
}; };
const openDevTools = () => {
w.value.webContents.openDevTools();
};
const reload = () => {
w.value.reload();
};
const onResize = () => {
isMaximized.value = w.value.isMaximized();
};
watch(windowTitle, (val) => {
ipcRenderer.send('change-window-title', val);
});
window.addEventListener('resize', onResize);
onUnmounted(() => {
window.removeEventListener('resize', onResize);
});
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -171,7 +146,7 @@ export default {
height: $titlebar-height; height: $titlebar-height;
line-height: 0; line-height: 0;
padding: 0 0.7rem; padding: 0 0.7rem;
opacity: 0.7; opacity: 0.9;
transition: opacity 0.2s; transition: opacity 0.2s;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;

View File

@@ -315,10 +315,9 @@
</template> </template>
</Draggable> </Draggable>
<WorkspaceEmptyState v-if="!workspace.tabs.length" @new-tab="addQueryTab" /> <WorkspaceEmptyState v-if="!workspace.tabs.length" @new-tab="addQueryTab" />
<template v-for="tab of workspace.tabs"> <template v-for="tab of workspace.tabs" :key="tab.uid">
<WorkspaceTabQuery <WorkspaceTabQuery
v-if="tab.type==='query'" v-if="tab.type ==='query'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:tab="tab" :tab="tab"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -326,7 +325,7 @@
/> />
<WorkspaceTabTable <WorkspaceTabTable
v-else-if="['temp-data', 'data'].includes(tab.type)" v-else-if="['temp-data', 'data'].includes(tab.type)"
:key="tab.uid" v-once
:tab-uid="tab.uid" :tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -336,7 +335,6 @@
/> />
<WorkspaceTabNewTable <WorkspaceTabNewTable
v-else-if="tab.type === 'new-table'" v-else-if="tab.type === 'new-table'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
@@ -345,7 +343,6 @@
/> />
<WorkspaceTabPropsTable <WorkspaceTabPropsTable
v-else-if="tab.type === 'table-props'" v-else-if="tab.type === 'table-props'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -354,7 +351,6 @@
/> />
<WorkspaceTabNewView <WorkspaceTabNewView
v-else-if="tab.type === 'new-view'" v-else-if="tab.type === 'new-view'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
@@ -363,7 +359,6 @@
/> />
<WorkspaceTabPropsView <WorkspaceTabPropsView
v-else-if="tab.type === 'view-props'" v-else-if="tab.type === 'view-props'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:connection="connection" :connection="connection"
@@ -372,7 +367,6 @@
/> />
<WorkspaceTabNewTrigger <WorkspaceTabNewTrigger
v-else-if="tab.type === 'new-trigger'" v-else-if="tab.type === 'new-trigger'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
@@ -382,7 +376,6 @@
/> />
<WorkspaceTabPropsTrigger <WorkspaceTabPropsTrigger
v-else-if="['temp-trigger-props', 'trigger-props'].includes(tab.type)" v-else-if="['temp-trigger-props', 'trigger-props'].includes(tab.type)"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -391,7 +384,6 @@
/> />
<WorkspaceTabNewTriggerFunction <WorkspaceTabNewTriggerFunction
v-else-if="tab.type === 'new-trigger-function'" v-else-if="tab.type === 'new-trigger-function'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
@@ -401,7 +393,6 @@
/> />
<WorkspaceTabPropsTriggerFunction <WorkspaceTabPropsTriggerFunction
v-else-if="['temp-trigger-function-props', 'trigger-function-props'].includes(tab.type)" v-else-if="['temp-trigger-function-props', 'trigger-function-props'].includes(tab.type)"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -410,7 +401,6 @@
/> />
<WorkspaceTabNewRoutine <WorkspaceTabNewRoutine
v-else-if="tab.type === 'new-routine'" v-else-if="tab.type === 'new-routine'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
@@ -420,7 +410,6 @@
/> />
<WorkspaceTabPropsRoutine <WorkspaceTabPropsRoutine
v-else-if="['temp-routine-props', 'routine-props'].includes(tab.type)" v-else-if="['temp-routine-props', 'routine-props'].includes(tab.type)"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -429,7 +418,6 @@
/> />
<WorkspaceTabNewFunction <WorkspaceTabNewFunction
v-else-if="tab.type === 'new-function'" v-else-if="tab.type === 'new-function'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
@@ -439,7 +427,6 @@
/> />
<WorkspaceTabPropsFunction <WorkspaceTabPropsFunction
v-else-if="['temp-function-props', 'function-props'].includes(tab.type)" v-else-if="['temp-function-props', 'function-props'].includes(tab.type)"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -448,7 +435,6 @@
/> />
<WorkspaceTabNewScheduler <WorkspaceTabNewScheduler
v-else-if="tab.type === 'new-scheduler'" v-else-if="tab.type === 'new-scheduler'"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:tab="tab" :tab="tab"
:connection="connection" :connection="connection"
@@ -458,7 +444,6 @@
/> />
<WorkspaceTabPropsScheduler <WorkspaceTabPropsScheduler
v-else-if="['temp-scheduler-props', 'scheduler-props'].includes(tab.type)" v-else-if="['temp-scheduler-props', 'scheduler-props'].includes(tab.type)"
:key="tab.uid"
:tab-uid="tab.uid" :tab-uid="tab.uid"
:connection="connection" :connection="connection"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
@@ -484,246 +469,196 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, onBeforeUnmount, Prop, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import Draggable from 'vuedraggable'; import * as Draggable from 'vuedraggable';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore, WorkspaceTab } from '@/stores/workspaces';
import { ConnectionParams } from 'common/interfaces/antares';
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState'; import WorkspaceEmptyState from '@/components/WorkspaceEmptyState.vue';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar'; import WorkspaceExploreBar from '@/components/WorkspaceExploreBar.vue';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel'; import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel.vue';
import WorkspaceTabQuery from '@/components/WorkspaceTabQuery'; import WorkspaceTabQuery from '@/components/WorkspaceTabQuery.vue';
import WorkspaceTabTable from '@/components/WorkspaceTabTable'; import WorkspaceTabTable from '@/components/WorkspaceTabTable.vue';
import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable'; import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable.vue';
import WorkspaceTabNewView from '@/components/WorkspaceTabNewView'; import WorkspaceTabNewView from '@/components/WorkspaceTabNewView.vue';
import WorkspaceTabNewTrigger from '@/components/WorkspaceTabNewTrigger'; import WorkspaceTabNewTrigger from '@/components/WorkspaceTabNewTrigger.vue';
import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine'; import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine.vue';
import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction'; import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction.vue';
import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler'; import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler.vue';
import WorkspaceTabNewTriggerFunction from '@/components/WorkspaceTabNewTriggerFunction'; import WorkspaceTabNewTriggerFunction from '@/components/WorkspaceTabNewTriggerFunction.vue';
import WorkspaceTabPropsTable from '@/components/WorkspaceTabPropsTable'; import WorkspaceTabPropsTable from '@/components/WorkspaceTabPropsTable.vue';
import WorkspaceTabPropsView from '@/components/WorkspaceTabPropsView'; import WorkspaceTabPropsView from '@/components/WorkspaceTabPropsView.vue';
import WorkspaceTabPropsTrigger from '@/components/WorkspaceTabPropsTrigger'; import WorkspaceTabPropsTrigger from '@/components/WorkspaceTabPropsTrigger.vue';
import WorkspaceTabPropsTriggerFunction from '@/components/WorkspaceTabPropsTriggerFunction'; import WorkspaceTabPropsTriggerFunction from '@/components/WorkspaceTabPropsTriggerFunction.vue';
import WorkspaceTabPropsRoutine from '@/components/WorkspaceTabPropsRoutine'; import WorkspaceTabPropsRoutine from '@/components/WorkspaceTabPropsRoutine.vue';
import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction'; import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction.vue';
import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler'; import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler.vue';
import ModalProcessesList from '@/components/ModalProcessesList'; import ModalProcessesList from '@/components/ModalProcessesList.vue';
import ModalDiscardChanges from '@/components/ModalDiscardChanges'; import ModalDiscardChanges from '@/components/ModalDiscardChanges.vue';
export default { const workspacesStore = useWorkspacesStore();
name: 'Workspace',
components: { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
Draggable,
WorkspaceEmptyState, const {
WorkspaceExploreBar, getWorkspace,
WorkspaceEditConnectionPanel, addWorkspace,
WorkspaceTabQuery, connectWorkspace,
WorkspaceTabTable, selectTab,
WorkspaceTabNewTable, newTab,
WorkspaceTabPropsTable, removeTab,
WorkspaceTabNewView, updateTabs
WorkspaceTabPropsView, } = workspacesStore;
WorkspaceTabNewTrigger,
WorkspaceTabPropsTrigger, const props = defineProps({
WorkspaceTabNewTriggerFunction, connection: Object as Prop<ConnectionParams>
WorkspaceTabPropsTriggerFunction, });
WorkspaceTabNewRoutine,
WorkspaceTabNewFunction, const hasWheelEvent = ref(false);
WorkspaceTabPropsRoutine, const isProcessesModal = ref(false);
WorkspaceTabPropsFunction, const unsavedTab = ref(null);
WorkspaceTabNewScheduler, const tabWrap = ref(null);
WorkspaceTabPropsScheduler,
ModalProcessesList, const workspace = computed(() => getWorkspace(props.connection.uid));
ModalDiscardChanges
const draggableTabs = computed<WorkspaceTab[]>({
get () {
return workspace.value.tabs;
}, },
props: { set (val) {
connection: Object updateTabs({ uid: props.connection.uid, tabs: val });
}, }
setup () { });
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const isSelected = computed(() => {
return selectedWorkspace.value === props.connection.uid;
});
const { const selectedTab = computed(() => {
getWorkspace, return workspace.value ? workspace.value.selectedTab : null;
addWorkspace, });
connectWorkspace,
removeConnected,
selectTab,
newTab,
removeTab,
updateTabs
} = workspacesStore;
return { const queryTabs = computed(() => {
selectedWorkspace, return workspace.value ? workspace.value.tabs.filter(tab => tab.type === 'query') : [];
getWorkspace, });
addWorkspace,
connectWorkspace,
removeConnected,
selectTab,
newTab,
removeTab,
updateTabs
};
},
data () {
return {
hasWheelEvent: false,
isProcessesModal: false,
unsavedTab: null
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
draggableTabs: {
get () {
return this.workspace.tabs;
},
set (val) {
this.updateTabs({ uid: this.connection.uid, tabs: val });
}
},
isSelected () {
return this.selectedWorkspace === this.connection.uid;
},
isSettingSupported () {
if (this.workspace.breadcrumbs.table && this.workspace.customizations.tableSettings) return true;
if (this.workspace.breadcrumbs.view && this.workspace.customizations.viewSettings) return true;
if (this.workspace.breadcrumbs.trigger && this.workspace.customizations.triggerSettings) return true;
if (this.workspace.breadcrumbs.procedure && this.workspace.customizations.routineSettings) return true;
if (this.workspace.breadcrumbs.function && this.workspace.customizations.functionSettings) return true;
if (this.workspace.breadcrumbs.triggerFunction && this.workspace.customizations.functionSettings) return true;
if (this.workspace.breadcrumbs.scheduler && this.workspace.customizations.schedulerSettings) return true;
return false;
},
selectedTab () {
return this.workspace ? this.workspace.selectedTab : null;
},
queryTabs () {
return this.workspace ? this.workspace.tabs.filter(tab => tab.type === 'query') : [];
},
schemaChild () {
for (const key in this.workspace.breadcrumbs) {
if (key === 'schema') continue;
if (this.workspace.breadcrumbs[key]) return this.workspace.breadcrumbs[key];
}
return false;
},
hasTools () {
if (!this.workspace.customizations) return false;
else {
return this.workspace.customizations.processesList ||
this.workspace.customizations.usersManagement ||
this.workspace.customizations.variables;
}
}
},
watch: {
queryTabs: {
handler (newVal, oldVal) {
if (newVal.length > oldVal.length) {
setTimeout(() => {
const scroller = this.$refs.tabWrap;
if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth;
}, 0);
}
},
deep: true
}
},
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);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
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) const hasTools = computed(() => {
return; if (!workspace.value.customizations) return false;
else {
return workspace.value.customizations.processesList ||
workspace.value.customizations.usersManagement ||
workspace.value.customizations.variables;
}
});
if ((e.ctrlKey || e.metaKey) && e.keyCode === 84 && !e.altKey) { // CTRL|Command + t watch(queryTabs, (newVal, oldVal) => {
this.addQueryTab(); if (newVal.length > oldVal.length) {
} setTimeout(() => {
const scroller = tabWrap.value;
if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth;
}, 0);
}
});
if ((e.ctrlKey || e.metaKey) && e.keyCode === 87 && !e.altKey) { // CTRL|Command + w const addQueryTab = () => {
const currentTab = this.getSelectedTab(); newTab({ uid: props.connection.uid, type: 'query' });
if (currentTab) };
this.closeTab(currentTab);
}
},
openAsPermanentTab (tab) {
const permanentTabs = {
table: 'data',
view: 'data',
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
scheduler: 'scheduler-props'
};
this.newTab({ const getSelectedTab = () => {
uid: this.connection.uid, return workspace.value.tabs.find(tab => tab.uid === selectedTab.value);
schema: tab.schema, };
elementName: tab.elementName,
type: permanentTabs[tab.elementType],
elementType: tab.elementType
});
},
closeTab (tab, force) {
this.unsavedTab = null;
// if (tab.type === 'query' && this.queryTabs.length === 1) return;
if (!force && tab.isChanged) {
this.unsavedTab = tab;
return;
}
this.removeTab({ uid: this.connection.uid, tab: tab.uid }); const onKey = (e: KeyboardEvent) => {
}, e.stopPropagation();
showProcessesModal () {
this.isProcessesModal = true; if (!isSelected.value)
}, return;
hideProcessesModal () {
this.isProcessesModal = false; if ((e.ctrlKey || e.metaKey) && e.key === 't' && !e.altKey) { // CTRL|Command + t
}, addQueryTab();
addWheelEvent () { }
if (!this.hasWheelEvent) {
this.$refs.tabWrap.$el.addEventListener('wheel', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'w' && !e.altKey) { // CTRL|Command + w
if (e.deltaY > 0) this.$refs.tabWrap.$el.scrollLeft += 50; const currentTab = getSelectedTab();
else this.$refs.tabWrap.$el.scrollLeft -= 50; if (currentTab)
}); closeTab(currentTab);
this.hasWheelEvent = true;
}
},
cutText (string) {
const limit = 20;
const escapedString = string.replace(/\s{2,}/g, ' ');
if (escapedString.length > limit)
return `${escapedString.substr(0, limit)}...`;
return escapedString;
}
} }
}; };
const openAsPermanentTab = (tab: WorkspaceTab) => {
const permanentTabs = {
table: 'data',
view: 'data',
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
procedure: 'routine-props',
scheduler: 'scheduler-props'
} as {[key: string]: string};
newTab({
uid: props.connection.uid,
schema: tab.schema,
elementName: tab.elementName,
type: permanentTabs[tab.elementType],
elementType: tab.elementType
});
};
const closeTab = (tab: WorkspaceTab, force = false) => {
unsavedTab.value = null;
// if (tab.type === 'query' && this.queryTabs.length === 1) return;
if (!force && tab.isChanged) {
unsavedTab.value = tab;
return;
}
removeTab({ uid: props.connection.uid, tab: tab.uid });
};
const showProcessesModal = () => {
isProcessesModal.value = true;
};
const hideProcessesModal = () => {
isProcessesModal.value = false;
};
const addWheelEvent = () => {
if (!hasWheelEvent.value) {
tabWrap.value.$el.addEventListener('wheel', (e: WheelEvent) => {
if (e.deltaY > 0) tabWrap.value.$el.scrollLeft += 50;
else tabWrap.value.$el.scrollLeft -= 50;
});
hasWheelEvent.value = true;
}
};
const cutText = (string: string) => {
const limit = 20;
const escapedString = string.replace(/\s{2,}/g, ' ');
if (escapedString.length > limit)
return `${escapedString.substr(0, limit)}...`;
return escapedString;
};
(async () => {
window.addEventListener('keydown', onKey);
await addWorkspace(props.connection.uid);
const isInitiated = await Connection.checkConnection(props.connection.uid);
if (isInitiated)
connectWorkspace(props.connection);
})();
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -8,23 +8,23 @@
:class="{'active': selectedTab === 'general'}" :class="{'active': selectedTab === 'general'}"
@click="selectTab('general')" @click="selectTab('general')"
> >
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ t('word.general') }}</a>
</li> </li>
<li <li
v-if="customizations.sslConnection" v-if="clientCustomizations.sslConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}" :class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')" @click="selectTab('ssl')"
> >
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ t('word.ssl') }}</a>
</li> </li>
<li <li
v-if="customizations.sshConnection" v-if="clientCustomizations.sshConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}" :class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')" @click="selectTab('ssh')"
> >
<a class="tab-link">{{ $t('word.sshTunnel') }}</a> <a class="tab-link">{{ t('word.sshTunnel') }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -34,7 +34,7 @@
<fieldset class="m-0" :disabled="isBusy"> <fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label> <label class="form-label cut-text">{{ t('word.connectionName') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -47,27 +47,21 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.client') }}</label> <label class="form-label cut-text">{{ t('word.client') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<select <BaseSelect
id="connection-client"
v-model="connection.client" v-model="connection.client"
:options="clients"
option-track-by="slug"
option-label="name"
class="form-select" class="form-select"
> />
<option
v-for="client in clients"
:key="client.slug"
:value="client.slug"
>
{{ client.name }}
</option>
</select>
</div> </div>
</div> </div>
<div v-if="connection.client === 'pg'" class="form-group columns"> <div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label> <label class="form-label cut-text">{{ t('word.connectionString') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -78,9 +72,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label> <label class="form-label cut-text">{{ t('word.hostName') }}/IP</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -90,22 +84,22 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.fileConnection" class="form-group columns"> <div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.database') }}</label> <label class="form-label cut-text">{{ t('word.database') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.databasePath" :model-value="connection.databasePath"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('databasePath')" @clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')" @change="pathSelection($event, 'databasePath')"
/> />
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.port') }}</label> <label class="form-label cut-text">{{ t('word.port') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -117,9 +111,9 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.database" class="form-group columns"> <div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.database') }}</label> <label class="form-label cut-text">{{ t('word.database') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -129,9 +123,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.user') }}</label> <label class="form-label cut-text">{{ t('word.user') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -142,9 +136,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.password') }}</label> <label class="form-label cut-text">{{ t('word.password') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -155,32 +149,32 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.connectionSchema" class="form-group columns"> <div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.schema') }}</label> <label class="form-label cut-text">{{ t('word.schema') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
v-model="connection.schema" v-model="connection.schema"
class="form-input" class="form-input"
type="text" type="text"
:placeholder="$t('word.all')" :placeholder="t('word.all')"
> >
</div> </div>
</div> </div>
<div v-if="customizations.readOnlyMode" class="form-group columns"> <div v-if="clientCustomizations.readOnlyMode" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }} <input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('message.readOnlyMode') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }} <input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('message.askCredentials') }}
</label> </label>
</div> </div>
</div> </div>
@@ -194,7 +188,7 @@
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text"> <label class="form-label cut-text">
{{ $t('message.enableSsl') }} {{ t('message.enableSsl') }}
</label> </label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
@@ -207,12 +201,12 @@
<fieldset class="m-0" :disabled="isBusy || !connection.ssl"> <fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label> <label class="form-label cut-text">{{ t('word.privateKey') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.key" :model-value="connection.key"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('key')" @clear="pathClear('key')"
@change="pathSelection($event, 'key')" @change="pathSelection($event, 'key')"
/> />
@@ -220,12 +214,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.certificate') }}</label> <label class="form-label cut-text">{{ t('word.certificate') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.cert" :model-value="connection.cert"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('cert')" @clear="pathClear('cert')"
@change="pathSelection($event, 'cert')" @change="pathSelection($event, 'cert')"
/> />
@@ -233,12 +227,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label> <label class="form-label cut-text">{{ t('word.caCertificate') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.ca" :model-value="connection.ca"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('ca')" @clear="pathClear('ca')"
@change="pathSelection($event, 'ca')" @change="pathSelection($event, 'ca')"
/> />
@@ -246,7 +240,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label> <label class="form-label cut-text">{{ t('word.ciphers') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -261,7 +255,7 @@
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ $t('message.untrustedConnection') }} <input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('message.untrustedConnection') }}
</label> </label>
</div> </div>
</div> </div>
@@ -275,7 +269,7 @@
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text"> <label class="form-label cut-text">
{{ $t('message.enableSsh') }} {{ t('message.enableSsh') }}
</label> </label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
@@ -288,7 +282,7 @@
<fieldset class="m-0" :disabled="isBusy || !connection.ssh"> <fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label> <label class="form-label cut-text">{{ t('word.hostName') }}/IP</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -300,7 +294,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.user') }}</label> <label class="form-label cut-text">{{ t('word.user') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -312,7 +306,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.password') }}</label> <label class="form-label cut-text">{{ t('word.password') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -324,7 +318,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.port') }}</label> <label class="form-label cut-text">{{ t('word.port') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -338,12 +332,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label> <label class="form-label cut-text">{{ t('word.privateKey') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="connection.sshKey" :model-value="connection.sshKey"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('sshKey')" @clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')" @change="pathSelection($event, 'sshKey')"
/> />
@@ -351,7 +345,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label> <label class="form-label cut-text">{{ t('word.passphrase') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -374,7 +368,7 @@
@click="startTest" @click="startTest"
> >
<i class="mdi mdi-24px mdi-lightning-bolt mr-1" /> <i class="mdi mdi-24px mdi-lightning-bolt mr-1" />
{{ $t('message.testConnection') }} {{ t('message.testConnection') }}
</button> </button>
<button <button
id="connection-save" id="connection-save"
@@ -383,7 +377,7 @@
@click="saveConnection" @click="saveConnection"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
{{ $t('word.save') }} {{ t('word.save') }}
</button> </button>
</div> </div>
</div> </div>
@@ -395,186 +389,171 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, Ref, ref, watch } from 'vue';
import customizations from 'common/customizations'; import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials.vue';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
export default { const { t } = useI18n();
name: 'WorkspaceAddConnectionPanel',
components: {
ModalAskCredentials,
BaseUploadInput
},
setup () {
const { addConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { connectWorkspace, selectWorkspace } = workspacesStore; const { addConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return { const { connectWorkspace, selectWorkspace } = workspacesStore;
addConnection,
addNotification, const clients = ref([
connectWorkspace, { name: 'MySQL', slug: 'mysql' },
selectWorkspace { name: 'MariaDB', slug: 'maria' },
}; { name: 'PostgreSQL', slug: 'pg' },
}, { name: 'SQLite', slug: 'sqlite' }
data () { ]);
return {
clients: [ const connection = ref({
{ name: 'MySQL', slug: 'mysql' }, name: '',
{ name: 'MariaDB', slug: 'maria' }, client: 'mysql',
{ name: 'PostgreSQL', slug: 'pg' }, host: '127.0.0.1',
{ name: 'SQLite', slug: 'sqlite' } database: null,
], databasePath: '',
connection: { port: null,
name: '', user: null,
client: 'mysql', password: '',
host: '127.0.0.1', ask: false,
database: null, readonly: false,
databasePath: '', uid: uidGen('C'),
port: null, ssl: false,
user: null, cert: '',
password: '', key: '',
ask: false, ca: '',
readonly: false, ciphers: '',
uid: uidGen('C'), untrustedConnection: false,
ssl: false, ssh: false,
cert: '', sshHost: '',
key: '', sshUser: '',
ca: '', sshPass: '',
ciphers: '', sshKey: '',
untrustedConnection: false, sshPort: 22,
ssh: false, pgConnString: ''
sshHost: '', }) as Ref<ConnectionParams & { pgConnString: string }>;
sshUser: '',
sshPass: '', const firstInput: Ref<HTMLInputElement> = ref(null);
sshKey: '', const isConnecting = ref(false);
sshPort: 22, const isTesting = ref(false);
pgConnString: '' const isAsking = ref(false);
}, const selectedTab = ref('general');
isConnecting: false,
isTesting: false, const clientCustomizations = computed(() => {
isAsking: false, return customizations[connection.value.client];
selectedTab: 'general' });
};
}, const isBusy = computed(() => {
computed: { return isConnecting.value || isTesting.value;
customizations () { });
return customizations[this.connection.client];
}, watch(() => connection.value.client, () => {
isBusy () { connection.value.user = clientCustomizations.value.defaultUser;
return this.isConnecting || this.isTesting; connection.value.port = clientCustomizations.value.defaultPort;
connection.value.database = clientCustomizations.value.defaultDatabase;
});
const setDefaults = () => {
connection.value.user = clientCustomizations.value.defaultUser;
connection.value.port = clientCustomizations.value.defaultPort;
connection.value.database = clientCustomizations.value.defaultDatabase;
};
const startTest = async () => {
isTesting.value = true;
if (connection.value.ask)
isAsking.value = true;
else {
try {
const res = await Connection.makeTest(connection.value);
if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
addNotification({ status: 'success', message: t('message.connectionSuccessfullyMade') });
} }
}, catch (err) {
watch: { addNotification({ status: 'error', message: err.stack });
'connection.client' () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
} }
},
created () {
this.setDefaults();
setTimeout(() => { isTesting.value = false;
if (this.$refs.firstInput) this.$refs.firstInput.focus();
}, 20);
},
methods: {
setDefaults () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
},
async startConnection () {
await this.saveConnection();
this.isConnecting = true;
if (this.connection.ask)
this.isAsking = true;
else {
await this.connectWorkspace(this.connection);
this.isConnecting = false;
}
},
async startTest () {
this.isTesting = true;
if (this.connection.ask)
this.isAsking = true;
else {
try {
const res = await Connection.makeTest(this.connection);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false;
}
},
async continueTest (credentials) { // if "Ask for credentials" is true
this.isAsking = false;
const params = Object.assign({}, this.connection, credentials);
try {
if (this.isConnecting) {
const params = Object.assign({}, this.connection, credentials);
await this.connectWorkspace(params);
this.isConnecting = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false;
},
saveConnection () {
this.selectWorkspace(this.connection.uid);
return this.addConnection(this.connection);
},
closeAsking () {
this.isTesting = false;
this.isAsking = false;
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.connection.ssl = !this.connection.ssl;
},
toggleSsh () {
this.connection.ssh = !this.connection.ssh;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.connection[name] = files[0].path;
},
pathClear (name) {
this.connection[name] = '';
}
} }
}; };
const continueTest = async (credentials: { user: string; password: string }) => { // if "Ask for credentials" is true
isAsking.value = false;
const params = Object.assign({}, connection.value, credentials);
try {
if (isConnecting.value) {
await connectWorkspace(params);
isConnecting.value = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
addNotification({ status: 'success', message: t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isTesting.value = false;
};
const saveConnection = async () => {
await addConnection(connection.value);
selectWorkspace(connection.value.uid);
};
const closeAsking = () => {
isTesting.value = false;
isAsking.value = false;
};
const selectTab = (tab: string) => {
selectedTab.value = tab;
};
const toggleSsl = () => {
connection.value.ssl = !connection.value.ssl;
};
const toggleSsh = () => {
connection.value.ssh = !connection.value.ssh;
};
const pathSelection = (event: Event & {target: {files: {path: string}[]}}, name: keyof ConnectionParams) => {
const { files } = event.target;
if (!files.length) return;
(connection.value as unknown as {[key: string]: string})[name] = files[0].path as string;
};
const pathClear = (name: keyof ConnectionParams) => {
(connection.value as unknown as {[key: string]: string})[name] = '';
};
setDefaults();
setTimeout(() => {
if (firstInput.value) firstInput.value.focus();
}, 20);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -8,23 +8,23 @@
:class="{'active': selectedTab === 'general'}" :class="{'active': selectedTab === 'general'}"
@click="selectTab('general')" @click="selectTab('general')"
> >
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ t('word.general') }}</a>
</li> </li>
<li <li
v-if="customizations.sslConnection" v-if="clientCustomizations.sslConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}" :class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')" @click="selectTab('ssl')"
> >
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ t('word.ssl') }}</a>
</li> </li>
<li <li
v-if="customizations.sshConnection" v-if="clientCustomizations.sshConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}" :class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')" @click="selectTab('ssh')"
> >
<a class="tab-link">{{ $t('word.sshTunnel') }}</a> <a class="tab-link">{{ t('word.sshTunnel') }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -34,7 +34,7 @@
<fieldset class="m-0" :disabled="isBusy"> <fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label> <label class="form-label cut-text">{{ t('word.connectionName') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -47,23 +47,23 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.client') }}</label> <label class="form-label cut-text">{{ t('word.client') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<select v-model="localConnection.client" class="form-select"> <BaseSelect
<option v-model="localConnection.client"
v-for="client in clients" :options="clients"
:key="client.slug" option-track-by="slug"
:value="client.slug" option-label="name"
> class="form-select"
{{ client.name }} dropdown-container=".workspace .connection-panel-wrapper"
</option> :dropdown-offsets="{top: 10}"
</select> />
</div> </div>
</div> </div>
<div v-if="connection.client === 'pg'" class="form-group columns"> <div v-if="localConnection.client === 'pg'" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label> <label class="form-label cut-text">{{ t('word.connectionString') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -74,9 +74,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label> <label class="form-label cut-text">{{ t('word.hostName') }}/IP</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -86,22 +86,22 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.fileConnection" class="form-group columns"> <div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.database') }}</label> <label class="form-label cut-text">{{ t('word.database') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.databasePath" :model-value="localConnection.databasePath"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('databasePath')" @clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')" @change="pathSelection($event, 'databasePath')"
/> />
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.port') }}</label> <label class="form-label cut-text">{{ t('word.port') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -113,9 +113,9 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.database" class="form-group columns"> <div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.database') }}</label> <label class="form-label cut-text">{{ t('word.database') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -125,9 +125,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.user') }}</label> <label class="form-label cut-text">{{ t('word.user') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -138,9 +138,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.password') }}</label> <label class="form-label cut-text">{{ t('word.password') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -151,32 +151,32 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.connectionSchema" class="form-group columns"> <div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.schema') }}</label> <label class="form-label cut-text">{{ t('word.schema') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
v-model="localConnection.schema" v-model="localConnection.schema"
class="form-input" class="form-input"
type="text" type="text"
:placeholder="$t('word.all')" :placeholder="t('word.all')"
> >
</div> </div>
</div> </div>
<div v-if="customizations.readOnlyMode" class="form-group columns"> <div v-if="clientCustomizations.readOnlyMode" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }} <input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ t('message.readOnlyMode') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }} <input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ t('message.askCredentials') }}
</label> </label>
</div> </div>
</div> </div>
@@ -190,7 +190,7 @@
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text"> <label class="form-label cut-text">
{{ $t('message.enableSsl') }} {{ t('message.enableSsl') }}
</label> </label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
@@ -203,12 +203,12 @@
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssl"> <fieldset class="m-0" :disabled="isBusy || !localConnection.ssl">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label> <label class="form-label cut-text">{{ t('word.privateKey') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.key" :model-value="localConnection.key"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('key')" @clear="pathClear('key')"
@change="pathSelection($event, 'key')" @change="pathSelection($event, 'key')"
/> />
@@ -216,12 +216,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.certificate') }}</label> <label class="form-label cut-text">{{ t('word.certificate') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.cert" :model-value="localConnection.cert"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('cert')" @clear="pathClear('cert')"
@change="pathSelection($event, 'cert')" @change="pathSelection($event, 'cert')"
/> />
@@ -229,12 +229,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label> <label class="form-label cut-text">{{ t('word.caCertificate') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.ca" :model-value="localConnection.ca"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('ca')" @clear="pathClear('ca')"
@change="pathSelection($event, 'ca')" @change="pathSelection($event, 'ca')"
/> />
@@ -242,7 +242,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label> <label class="form-label cut-text">{{ t('word.ciphers') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -263,7 +263,7 @@
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text"> <label class="form-label cut-text">
{{ $t('message.enableSsh') }} {{ t('message.enableSsh') }}
</label> </label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
@@ -276,7 +276,7 @@
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssh"> <fieldset class="m-0" :disabled="isBusy || !localConnection.ssh">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label> <label class="form-label cut-text">{{ t('word.hostName') }}/IP</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -288,7 +288,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.user') }}</label> <label class="form-label cut-text">{{ t('word.user') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -300,7 +300,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.password') }}</label> <label class="form-label cut-text">{{ t('word.password') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -312,7 +312,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.port') }}</label> <label class="form-label cut-text">{{ t('word.port') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -326,12 +326,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label> <label class="form-label cut-text">{{ t('word.privateKey') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:value="localConnection.sshKey" :model-value="localConnection.sshKey"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('sshKey')" @clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')" @change="pathSelection($event, 'sshKey')"
/> />
@@ -339,7 +339,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label> <label class="form-label cut-text">{{ t('word.passphrase') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@@ -362,7 +362,7 @@
@click="startTest" @click="startTest"
> >
<i class="mdi mdi-24px mdi-lightning-bolt mr-1" /> <i class="mdi mdi-24px mdi-lightning-bolt mr-1" />
{{ $t('message.testConnection') }} {{ t('message.testConnection') }}
</button> </button>
<button <button
id="connection-save" id="connection-save"
@@ -371,7 +371,7 @@
@click="saveConnection" @click="saveConnection"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
{{ $t('word.save') }} {{ t('word.save') }}
</button> </button>
<button <button
id="connection-connect" id="connection-connect"
@@ -381,7 +381,7 @@
@click="startConnection" @click="startConnection"
> >
<i class="mdi mdi-24px mdi-connection mr-1" /> <i class="mdi mdi-24px mdi-connection mr-1" />
{{ $t('word.connect') }} {{ t('word.connect') }}
</button> </button>
</div> </div>
</div> </div>
@@ -393,152 +393,150 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, Prop, Ref, ref, watch } from 'vue';
import customizations from 'common/customizations'; import customizations from 'common/customizations';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials.vue';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
export default { const { t } = useI18n();
name: 'WorkspaceEditConnectionPanel',
components: {
ModalAskCredentials,
BaseUploadInput
},
props: {
connection: Object
},
setup () {
const { editConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const { connectWorkspace } = useWorkspacesStore();
return { const props = defineProps({
editConnection, connection: Object as Prop<ConnectionParams>
addNotification, });
connectWorkspace
};
},
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,
localConnection: null,
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.localConnection.client];
},
isBusy () {
return this.isConnecting || this.isTesting;
},
hasChanges () {
return JSON.stringify(this.connection) !== JSON.stringify(this.localConnection);
}
},
watch: {
connection () {
this.localConnection = JSON.parse(JSON.stringify(this.connection));
}
},
created () {
this.localConnection = JSON.parse(JSON.stringify(this.connection));
},
methods: {
async startConnection () {
await this.saveConnection();
this.isConnecting = true;
if (this.localConnection.ask) const { editConnection } = useConnectionsStore();
this.isAsking = true; const { addNotification } = useNotificationsStore();
else { const { connectWorkspace } = useWorkspacesStore();
await this.connectWorkspace(this.localConnection);
this.isConnecting = false;
}
},
async startTest () {
this.isTesting = true;
if (this.localConnection.ask) const clients = ref([
this.isAsking = true; { name: 'MySQL', slug: 'mysql' },
else { { name: 'MariaDB', slug: 'maria' },
try { { name: 'PostgreSQL', slug: 'pg' },
const res = await Connection.makeTest(this.localConnection); { name: 'SQLite', slug: 'sqlite' }
if (res.status === 'error') ]);
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false; const firstInput: Ref<HTMLInputElement> = ref(null);
} const localConnection: Ref<ConnectionParams & { pgConnString: string }> = ref(null);
}, const isConnecting = ref(false);
async continueTest (credentials) { // if "Ask for credentials" is true const isTesting = ref(false);
this.isAsking = false; const isAsking = ref(false);
const params = Object.assign({}, this.localConnection, credentials); const selectedTab = ref('general');
try {
if (this.isConnecting) {
const params = Object.assign({}, this.connection, credentials);
await this.connectWorkspace(params);
this.isConnecting = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false; const clientCustomizations = computed(() => {
}, return customizations[localConnection.value.client];
saveConnection () { });
return this.editConnection(this.localConnection);
},
closeAsking () {
this.isTesting = false;
this.isAsking = false;
this.isConnecting = false;
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.localConnection.ssl = !this.localConnection.ssl;
},
toggleSsh () {
this.localConnection.ssh = !this.localConnection.ssh;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.localConnection[name] = files[0].path; const isBusy = computed(() => {
}, return isConnecting.value || isTesting.value;
pathClear (name) { });
this.localConnection[name] = '';
} const hasChanges = computed(() => {
return JSON.stringify(props.connection) !== JSON.stringify(localConnection.value);
});
watch(() => props.connection, () => {
localConnection.value = JSON.parse(JSON.stringify(props.connection));
});
const startConnection = async () => {
await saveConnection();
isConnecting.value = true;
if (localConnection.value.ask)
isAsking.value = true;
else {
await connectWorkspace(localConnection.value);
isConnecting.value = false;
} }
}; };
const startTest = async () => {
isTesting.value = true;
if (localConnection.value.ask)
isAsking.value = true;
else {
try {
const res = await Connection.makeTest(localConnection.value);
if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
addNotification({ status: 'success', message: t('message.connectionSuccessfullyMade') });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isTesting.value = false;
}
};
const continueTest = async (credentials: {user: string; password: string }) => { // if "Ask for credentials" is true
isAsking.value = false;
const params = Object.assign({}, localConnection.value, credentials);
try {
if (isConnecting.value) {
const params = Object.assign({}, props.connection, credentials);
await connectWorkspace(params);
isConnecting.value = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
addNotification({ status: 'success', message: t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isTesting.value = false;
};
const saveConnection = () => {
return editConnection(localConnection.value);
};
const closeAsking = () => {
isTesting.value = false;
isAsking.value = false;
isConnecting.value = false;
};
const selectTab = (tab: string) => {
selectedTab.value = tab;
};
const toggleSsl = () => {
localConnection.value.ssl = !localConnection.value.ssl;
};
const toggleSsh = () => {
localConnection.value.ssh = !localConnection.value.ssh;
};
const pathSelection = (event: Event & {target: {files: {path: string}[]}}, name: keyof ConnectionParams) => {
const { files } = event.target;
if (!files.length) return;
(localConnection.value as unknown as {[key: string]: string})[name] = files[0].path;
};
const pathClear = (name: keyof ConnectionParams) => {
(localConnection.value as unknown as {[key: string]: string})[name] = '';
};
localConnection.value = JSON.parse(JSON.stringify(props.connection));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,61 +1,48 @@
<template> <template>
<div class="column col-12 empty"> <div class="column col-12 empty">
<div class="empty-icon"> <div class="empty-icon">
<img <img :src="logos[applicationTheme]" width="200">
v-if="applicationTheme === 'dark'"
src="../images/logo-dark.svg"
width="200"
>
<img
v-if="applicationTheme === 'light'"
src="../images/logo-light.svg"
width="200"
>
</div> </div>
<p class="h6 empty-subtitle"> <p class="h6 empty-subtitle">
{{ $t('message.noOpenTabs') }} {{ t('message.noOpenTabs') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-gray d-flex" @click="$emit('new-tab')"> <button class="btn btn-gray d-flex" @click="emit('new-tab')">
<i class="mdi mdi-24px mdi-tab-plus mr-2" /> <i class="mdi mdi-24px mdi-tab-plus mr-2" />
{{ $t('message.openNewTab') }} {{ t('message.openNewTab') }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts">
<script> import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n';
export default {
name: 'WorkspaceEmptyState',
emits: ['new-tab'],
setup () {
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { applicationTheme } = storeToRefs(settingsStore); const { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, changeBreadcrumbs } = workspacesStore; const emit = defineEmits(['new-tab']);
return { const logos = {
applicationTheme, light: require('../images/logo-light.svg') as string,
selectedWorkspace, dark: require('../images/logo-dark.svg') as string
getWorkspace,
changeBreadcrumbs
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
}
},
created () {
this.changeBreadcrumbs({ schema: this.workspace.breadcrumbs.schema });
}
}; };
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { applicationTheme } = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, changeBreadcrumbs } = workspacesStore;
const workspace = computed(() => {
return getWorkspace(selectedWorkspace.value);
});
changeBreadcrumbs({ schema: workspace.value.breadcrumbs.schema });
</script> </script>
<style scoped> <style scoped>

View File

@@ -48,7 +48,7 @@
/> />
</div> </div>
</div> </div>
<div class="workspace-explorebar-body" @click="$refs.explorebar.focus()"> <div class="workspace-explorebar-body" @click="explorebar.focus()">
<WorkspaceExploreBarSchema <WorkspaceExploreBarSchema
v-for="db of workspace.structure" v-for="db of workspace.structure"
:key="db.name" :key="db.name"
@@ -115,7 +115,8 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { Component, computed, onMounted, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
@@ -125,428 +126,290 @@ import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views'; import Views from '@/ipc-api/Views';
import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema'; import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema.vue';
import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext'; import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext.vue';
import TableContext from '@/components/WorkspaceExploreBarTableContext'; import TableContext from '@/components/WorkspaceExploreBarTableContext.vue';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext'; import MiscContext from '@/components/WorkspaceExploreBarMiscContext.vue';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext'; import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext.vue';
import ModalNewSchema from '@/components/ModalNewSchema'; import ModalNewSchema from '@/components/ModalNewSchema.vue';
export default { const props = defineProps({
name: 'WorkspaceExploreBar', connection: Object,
components: { isSelected: Boolean
WorkspaceExploreBarSchema, });
DatabaseContext,
TableContext,
MiscContext,
MiscFolderContext,
ModalNewSchema
},
props: {
connection: Object,
isSelected: Boolean
},
setup () {
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { explorebarSize } = storeToRefs(settingsStore); const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { changeExplorebarSize } = settingsStore; const { explorebarSize } = storeToRefs(settingsStore);
const {
getWorkspace,
removeConnected: disconnectWorkspace,
refreshStructure,
changeBreadcrumbs,
selectTab,
newTab,
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
} = workspacesStore;
return { const { changeExplorebarSize } = settingsStore;
getConnectionName, const {
addNotification, getWorkspace,
explorebarSize, removeConnected: disconnectWorkspace,
changeExplorebarSize, refreshStructure,
getWorkspace, newTab,
disconnectWorkspace, removeTabs,
refreshStructure, setSearchTerm,
changeBreadcrumbs, addLoadingElement,
selectTab, removeLoadingElement
newTab, } = workspacesStore;
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
};
},
data () {
return {
isRefreshing: false,
isNewDBModal: false, const searchInput: Ref<HTMLInputElement> = ref(null);
isNewViewModal: false, const explorebar: Ref<HTMLInputElement> = ref(null);
isNewTriggerModal: false, const resizer: Ref<HTMLInputElement> = ref(null);
isNewRoutineModal: false, const schema: Ref<Component & { selectSchema: (name: string) => void; $refs: {schemaAccordion: HTMLDetailsElement} }[]> = ref(null);
isNewFunctionModal: false, const isRefreshing = ref(false);
isNewTriggerFunctionModal: false, const isNewDBModal = ref(false);
isNewSchedulerModal: false, const localWidth = ref(null);
const explorebarWidthInterval = ref(null);
const searchTermInterval = ref(null);
const isDatabaseContext = ref(false);
const isTableContext = ref(false);
const isMiscContext = ref(false);
const isMiscFolderContext = ref(false);
const databaseContextEvent = ref(null);
const tableContextEvent = ref(null);
const miscContextEvent = ref(null);
const selectedSchema = ref('');
const selectedTable = ref(null);
const selectedMisc = ref(null);
const searchTerm = ref('');
localWidth: null, const workspace = computed(() => {
explorebarWidthInterval: null, return getWorkspace(props.connection.uid);
searchTermInterval: null, });
isDatabaseContext: false,
isTableContext: false,
isMiscContext: false,
isMiscFolderContext: false,
databaseContextEvent: null, const connectionName = computed(() => {
tableContextEvent: null, return getConnectionName(props.connection.uid);
miscContextEvent: null, });
selectedSchema: '', const customizations = computed(() => {
selectedTable: null, return workspace.value.customizations;
selectedMisc: null, });
searchTerm: ''
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
connectionName () {
return this.getConnectionName(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
}
},
watch: {
localWidth (val) {
clearTimeout(this.explorebarWidthInterval);
this.explorebarWidthInterval = setTimeout(() => { watch(localWidth, (val: number) => {
this.changeExplorebarSize(val); clearTimeout(explorebarWidthInterval.value);
}, 500);
},
isSelected (val) {
if (val) this.localWidth = this.explorebarSize;
},
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => { explorebarWidthInterval.value = setTimeout(() => {
this.setSearchTerm(this.searchTerm); changeExplorebarSize(val);
}, 200); }, 500);
} });
},
created () {
this.localWidth = this.explorebarSize;
},
mounted () {
const resizer = this.$refs.resizer;
resizer.addEventListener('mousedown', e => { watch(() => props.isSelected, (val: boolean) => {
e.preventDefault(); if (val) localWidth.value = explorebarSize.value;
});
window.addEventListener('mousemove', this.resize); watch(searchTerm, () => {
window.addEventListener('mouseup', this.stopResize); clearTimeout(searchTermInterval.value);
});
if (this.workspace.structure.length === 1) { // Auto-open if juust one schema searchTermInterval.value = setTimeout(() => {
this.$refs.schema[0].selectSchema(this.workspace.structure[0].name); setSearchTerm(searchTerm.value);
this.$refs.schema[0].$refs.schemaAccordion.open = true; }, 200);
} });
},
methods: {
async refresh () {
if (!this.isRefreshing) {
this.isRefreshing = true;
await this.refreshStructure(this.connection.uid);
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(); localWidth.value = explorebarSize.value;
},
resize (e) {
const el = this.$refs.explorebar;
let explorebarWidth = e.pageX - el.getBoundingClientRect().left;
if (explorebarWidth > 500) explorebarWidth = 500;
if (explorebarWidth < 150) explorebarWidth = 150;
this.localWidth = explorebarWidth;
},
stopResize () {
window.removeEventListener('mousemove', this.resize);
},
showNewDBModal () {
this.isNewDBModal = true;
},
hideNewDBModal () {
this.isNewDBModal = false;
},
openCreateElementTab (element) {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.newTab({ onMounted(() => {
uid: this.workspace.uid, resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
schema: this.selectedSchema, e.preventDefault();
elementName: '',
elementType: element,
type: `new-${element}`
});
},
openSchemaContext (payload) {
this.selectedSchema = payload.schema;
this.databaseContextEvent = payload.event;
this.isDatabaseContext = true;
},
closeDatabaseContext () {
this.isDatabaseContext = false;
},
openTableContext (payload) {
this.selectedTable = payload.table;
this.selectedSchema = payload.schema;
this.tableContextEvent = payload.event;
this.isTableContext = true;
},
closeTableContext () {
this.isTableContext = false;
},
openMiscContext (payload) {
this.selectedMisc = payload.misc;
this.selectedSchema = payload.schema;
this.miscContextEvent = payload.event;
this.isMiscContext = true;
},
openMiscFolderContext (payload) {
this.selectedMisc = payload.type;
this.selectedSchema = payload.schema;
this.miscContextEvent = payload.event;
this.isMiscFolderContext = true;
},
closeMiscContext () {
this.isMiscContext = false;
},
closeMiscFolderContext () {
this.isMiscFolderContext = false;
},
showCreateTriggerModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewTriggerModal = true;
},
hideCreateTriggerModal () {
this.isNewTriggerModal = false;
},
showCreateRoutineModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewRoutineModal = true;
},
hideCreateRoutineModal () {
this.isNewRoutineModal = false;
},
showCreateFunctionModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewFunctionModal = true;
},
hideCreateFunctionModal () {
this.isNewFunctionModal = false;
},
showCreateTriggerFunctionModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewTriggerFunctionModal = true;
},
hideCreateTriggerFunctionModal () {
this.isNewTriggerFunctionModal = false;
},
showCreateSchedulerModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewSchedulerModal = true;
},
hideCreateSchedulerModal () {
this.isNewSchedulerModal = false;
},
async deleteTable (payload) {
this.closeTableContext();
this.addLoadingElement({ window.addEventListener('mousemove', resize);
name: payload.table.name, window.addEventListener('mouseup', stopResize);
schema: payload.schema, });
type: 'table'
});
try { if (workspace.value.structure.length === 1) { // Auto-open if juust one schema
let res; schema.value[0].selectSchema(workspace.value.structure[0].name);
schema.value[0].$refs.schemaAccordion.open = true;
}
});
if (payload.table.type === 'table') { const refresh = async () => {
res = await Tables.dropTable({ if (!isRefreshing.value) {
uid: this.connection.uid, isRefreshing.value = true;
table: payload.table.name, await refreshStructure(props.connection.uid);
schema: payload.schema isRefreshing.value = false;
});
}
else if (payload.table.type === 'view') {
res = await Views.dropView({
uid: this.connection.uid,
view: payload.table.name,
schema: payload.schema
});
}
const { status, response } = res;
if (status === 'success') {
this.refresh();
this.removeTabs({
uid: this.connection.uid,
elementName: payload.table.name,
elementType: payload.table.type,
schema: payload.schema
});
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
},
async duplicateTable (payload) {
this.closeTableContext();
this.addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
const { status, response } = await Tables.duplicateTable({
uid: this.connection.uid,
table: payload.table.name,
schema: payload.schema
});
if (status === 'success')
this.refresh();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
},
async openCreateFunctionEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Functions.createFunction(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, function: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'function',
type: 'function-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
async openCreateTriggerFunctionEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Functions.createTriggerFunction(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, triggerFunction: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'triggerFunction',
type: 'trigger-function-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
async openCreateSchedulerEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Schedulers.createScheduler(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, scheduler: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'scheduler',
type: 'scheduler-props'
});
}
else
this.addNotification({ status: 'error', message: response });
}
} }
}; };
const explorebarSearch = (e: KeyboardEvent) => {
if (e.code === 'Backspace') {
e.preventDefault();
if (searchTerm.value.length)
searchTerm.value = searchTerm.value.slice(0, -1);
else
return;
}
else if (e.key.length > 1)// Prevent non-alphanumerics
return;
searchInput.value.focus();
};
const resize = (e: MouseEvent) => {
const el = explorebar.value;
let explorebarWidth = e.pageX - el.getBoundingClientRect().left;
if (explorebarWidth > 500) explorebarWidth = 500;
if (explorebarWidth < 150) explorebarWidth = 150;
localWidth.value = explorebarWidth;
};
const stopResize = () => {
window.removeEventListener('mousemove', resize);
};
const showNewDBModal = () => {
isNewDBModal.value = true;
};
const hideNewDBModal = () => {
isNewDBModal.value = false;
};
const openCreateElementTab = (element: string) => {
closeDatabaseContext();
closeMiscFolderContext();
newTab({
uid: workspace.value.uid,
schema: selectedSchema.value,
elementName: '',
elementType: element,
type: `new-${element}`
});
};
const openSchemaContext = (payload: { schema: string; event: PointerEvent }) => {
selectedSchema.value = payload.schema;
databaseContextEvent.value = payload.event;
isDatabaseContext.value = true;
};
const closeDatabaseContext = () => {
isDatabaseContext.value = false;
};
const openTableContext = (payload: { schema: string; table: string; event: PointerEvent }) => {
selectedTable.value = payload.table;
selectedSchema.value = payload.schema;
tableContextEvent.value = payload.event;
isTableContext.value = true;
};
const closeTableContext = () => {
isTableContext.value = false;
};
const openMiscContext = (payload: { schema: string; misc: string; event: PointerEvent }) => {
selectedMisc.value = payload.misc;
selectedSchema.value = payload.schema;
miscContextEvent.value = payload.event;
isMiscContext.value = true;
};
const openMiscFolderContext = (payload: { schema: string; type: string; event: PointerEvent }) => {
selectedMisc.value = payload.type;
selectedSchema.value = payload.schema;
miscContextEvent.value = payload.event;
isMiscFolderContext.value = true;
};
const closeMiscContext = () => {
isMiscContext.value = false;
};
const closeMiscFolderContext = () => {
isMiscFolderContext.value = false;
};
const deleteTable = async (payload: { schema: string; table: { name: string; type: string }; event: PointerEvent }) => {
closeTableContext();
addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
let res;
if (payload.table.type === 'table') {
res = await Tables.dropTable({
uid: props.connection.uid,
table: payload.table.name,
schema: payload.schema
});
}
else if (payload.table.type === 'view') {
res = await Views.dropView({
uid: props.connection.uid,
view: payload.table.name,
schema: payload.schema
});
}
const { status, response } = res;
if (status === 'success') {
refresh();
removeTabs({
uid: props.connection.uid as string,
elementName: payload.table.name as string,
elementType: payload.table.type,
schema: payload.schema as string
});
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
};
const duplicateTable = async (payload: { schema: string; table: { name: string }; event: PointerEvent }) => {
closeTableContext();
addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
const { status, response } = await Tables.duplicateTable({
uid: props.connection.uid,
table: payload.table.name,
schema: payload.schema
});
if (status === 'success')
refresh();
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -4,7 +4,7 @@
@close-context="closeContext" @close-context="closeContext"
> >
<div <div
v-if="['procedure', 'function'].includes(selectedMisc.type)" v-if="['procedure', 'routine', 'function'].includes(selectedMisc.type)"
class="context-element" class="context-element"
@click="runElementCheck" @click="runElementCheck"
> >
@@ -56,7 +56,7 @@
</ConfirmModal> </ConfirmModal>
<ModalAskParameters <ModalAskParameters
v-if="isAskingParameters" v-if="isAskingParameters"
:local-routine="localElement" :local-routine="(localElement as any)"
:client="workspace.client" :client="workspace.client"
@confirm="runElement" @confirm="runElement"
@close="hideAskParamsModal" @close="hideAskParamsModal"
@@ -64,324 +64,333 @@
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import { computed, Prop, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import ModalAskParameters from '@/components/ModalAskParameters'; import ModalAskParameters from '@/components/ModalAskParameters.vue';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers'; import Schedulers from '@/ipc-api/Schedulers';
import { storeToRefs } from 'pinia'; import { EventInfos, FunctionInfos, IpcResponse, RoutineInfos, TriggerInfos } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceExploreBarMiscContext',
components: {
BaseContextMenu,
ConfirmModal,
ModalAskParameters
},
props: {
contextEvent: MouseEvent,
selectedMisc: Object,
selectedSchema: String
},
emits: ['close-context', 'reload'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
contextEvent: MouseEvent,
selectedMisc: Object as Prop<{ name:string; type:string; enabled?: boolean }>,
selectedSchema: String
});
const { const emit = defineEmits(['close-context', 'reload']);
getWorkspace,
changeBreadcrumbs,
addLoadingElement,
removeLoadingElement,
removeTabs,
newTab
} = workspacesStore;
return { const { addNotification } = useNotificationsStore();
addNotification, const workspacesStore = useWorkspacesStore();
selectedWorkspace,
getWorkspace,
changeBreadcrumbs,
addLoadingElement,
removeLoadingElement,
removeTabs,
newTab
};
},
data () {
return {
isDeleteModal: false,
isRunModal: false,
isAskingParameters: false,
localElement: {}
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.getWorkspace(this.selectedWorkspace).customizations;
},
deleteMessage () {
switch (this.selectedMisc.type) {
case 'trigger':
return this.$t('message.deleteTrigger');
case 'procedure':
return this.$t('message.deleteRoutine');
case 'function':
case 'triggerFunction':
return this.$t('message.deleteFunction');
case 'scheduler':
return this.$t('message.deleteScheduler');
default:
return '';
}
}
},
methods: {
showDeleteModal () {
this.isDeleteModal = true;
},
hideDeleteModal () {
this.isDeleteModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
},
async deleteMisc () {
try {
let res;
switch (this.selectedMisc.type) { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
case 'trigger':
res = await Triggers.dropTrigger({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
trigger: this.selectedMisc.name
});
break;
case 'procedure':
res = await Routines.dropRoutine({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
routine: this.selectedMisc.name
});
break;
case 'function':
case 'triggerFunction':
res = await Functions.dropFunction({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
func: this.selectedMisc.name
});
break;
case 'scheduler':
res = await Schedulers.dropScheduler({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
scheduler: this.selectedMisc.name
});
break;
}
const { status, response } = res; const {
getWorkspace,
addLoadingElement,
removeLoadingElement,
removeTabs,
newTab
} = workspacesStore;
if (status === 'success') { const isDeleteModal = ref(false);
this.removeTabs({ const isAskingParameters = ref(false);
uid: this.selectedWorkspace, const localElement: Ref<TriggerInfos | RoutineInfos | FunctionInfos | EventInfos> = ref(null);
elementName: this.selectedMisc.name,
elementType: this.selectedMisc.type,
schema: this.selectedSchema
});
this.closeContext(); const workspace = computed(() => {
this.$emit('reload'); return getWorkspace(selectedWorkspace.value);
} });
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
runElementCheck () {
if (this.selectedMisc.type === 'procedure')
this.runRoutineCheck();
else if (this.selectedMisc.type === 'function')
this.runFunctionCheck();
},
runElement (params) {
if (this.selectedMisc.type === 'procedure')
this.runRoutine(params);
else if (this.selectedMisc.type === 'function')
this.runFunction(params);
},
async runRoutineCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.selectedSchema,
routine: this.selectedMisc.name
};
try { const customizations = computed(() => {
const { status, response } = await Routines.getRoutineInformations(params); return getWorkspace(selectedWorkspace.value).customizations;
if (status === 'success') });
this.localElement = response;
else const deleteMessage = computed(() => {
this.addNotification({ status: 'error', message: response }); switch (props.selectedMisc.type) {
} case 'trigger':
catch (err) { return t('message.deleteTrigger');
this.addNotification({ status: 'error', message: err.stack }); case 'procedure':
} return t('message.deleteRoutine');
case 'function':
case 'triggerFunction':
return t('message.deleteFunction');
case 'scheduler':
return t('message.deleteScheduler');
default:
return '';
}
});
if (this.localElement.parameters.length) const showDeleteModal = () => {
this.showAskParamsModal(); isDeleteModal.value = true;
else };
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql; const hideDeleteModal = () => {
switch (this.workspace.client) { // TODO: move in a better place isDeleteModal.value = false;
case 'maria': };
case 'mysql':
case 'pg':
sql = `CALL ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.localElement.name}\`(${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, type: 'query', autorun: true }); const showAskParamsModal = () => {
this.closeContext(); isAskingParameters.value = true;
}, };
async runFunctionCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.selectedSchema,
func: this.selectedMisc.name
};
try { const hideAskParamsModal = () => {
const { status, response } = await Functions.getFunctionInformations(params); isAskingParameters.value = false;
if (status === 'success') closeContext();
this.localElement = response; };
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length) const closeContext = () => {
this.showAskParamsModal(); emit('close-context');
else };
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql; const deleteMisc = async () => {
switch (this.workspace.client) { // TODO: move in a better place try {
case 'maria': let res: IpcResponse;
case 'mysql':
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, type: 'query', autorun: true }); switch (props.selectedMisc.type) {
this.closeContext(); case 'trigger':
}, res = await Triggers.dropTrigger({
async toggleTrigger () { uid: selectedWorkspace.value,
this.addLoadingElement({ schema: props.selectedSchema,
name: this.selectedMisc.name, trigger: props.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
}); });
break;
if (status !== 'success') case 'routine':
this.addNotification({ status: 'error', message: response }); case 'procedure':
} res = await Routines.dropRoutine({
catch (err) { uid: selectedWorkspace.value,
this.addNotification({ status: 'error', message: err.stack }); schema: props.selectedSchema,
} routine: props.selectedMisc.name
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
}); });
break;
case 'function':
case 'triggerFunction':
res = await Functions.dropFunction({
uid: selectedWorkspace.value,
schema: props.selectedSchema,
func: props.selectedMisc.name
});
break;
case 'scheduler':
res = await Schedulers.dropScheduler({
uid: selectedWorkspace.value,
schema: props.selectedSchema,
scheduler: props.selectedMisc.name
});
break;
}
if (status !== 'success') console.log(res);
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({ const { status, response } = res;
name: this.selectedMisc.name,
schema: this.selectedSchema, if (status === 'success') {
type: 'scheduler' removeTabs({
uid: selectedWorkspace.value,
elementName: props.selectedMisc.name,
elementType: props.selectedMisc.type,
schema: props.selectedSchema
}); });
this.closeContext(); closeContext();
this.$emit('reload'); emit('reload');
} }
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
} }
}; };
const runElementCheck = () => {
if (['procedure', 'routine'].includes(props.selectedMisc.type))
runRoutineCheck();
else if (props.selectedMisc.type === 'function')
runFunctionCheck();
};
const runElement = (params: string[]) => {
if (props.selectedMisc.type === 'procedure')
runRoutine(params);
else if (props.selectedMisc.type === 'function')
runFunction(params);
};
const runRoutineCheck = async () => {
const params = {
uid: selectedWorkspace.value,
schema: props.selectedSchema,
routine: props.selectedMisc.name
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success')
localElement.value = response;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
if ((localElement.value as RoutineInfos).parameters.length)
showAskParamsModal();
else
runRoutine();
};
const runRoutine = (params?: string[]) => {
if (!params) params = [];
let sql;
switch (workspace.value.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL ${localElement.value.name}(${params.join(',')})`;
break;
// case 'mssql':
// sql = `EXEC ${localElement.value.name} ${params.join(',')}`;
// break;
default:
sql = `CALL \`${localElement.value.name}\`(${params.join(',')})`;
}
newTab({
uid: workspace.value.uid,
content: sql,
type: 'query',
schema: props.selectedSchema,
autorun: true
});
closeContext();
};
const runFunctionCheck = async () => {
const params = {
uid: selectedWorkspace.value,
schema: props.selectedSchema,
func: props.selectedMisc.name
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success')
localElement.value = response;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
if ((localElement.value as FunctionInfos).parameters.length)
showAskParamsModal();
else
runFunction();
};
const runFunction = (params?: string[]) => {
if (!params) params = [];
let sql;
switch (workspace.value.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
sql = `SELECT \`${localElement.value.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${localElement.value.name}(${params.join(',')})`;
break;
// case 'mssql':
// sql = `SELECT ${localElement.value.name} ${params.join(',')}`;
// break;
default:
sql = `SELECT \`${localElement.value.name}\` (${params.join(',')})`;
}
newTab({
uid: workspace.value.uid,
content: sql,
type: 'query',
schema: props.selectedSchema,
autorun: true
});
closeContext();
};
const toggleTrigger = async () => {
addLoadingElement({
name: props.selectedMisc.name,
schema: props.selectedSchema,
type: 'trigger'
});
try {
const { status, response } = await Triggers.toggleTrigger({
uid: selectedWorkspace.value,
schema: props.selectedSchema,
trigger: props.selectedMisc.name,
enabled: props.selectedMisc.enabled
});
if (status !== 'success')
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: props.selectedMisc.name,
schema: props.selectedSchema,
type: 'trigger'
});
closeContext();
emit('reload');
};
const toggleScheduler = async () => {
addLoadingElement({
name: props.selectedMisc.name,
schema: props.selectedSchema,
type: 'scheduler'
});
try {
const { status, response } = await Schedulers.toggleScheduler({
uid: selectedWorkspace.value,
schema: props.selectedSchema,
scheduler: props.selectedMisc.name,
enabled: props.selectedMisc.enabled
});
if (status !== 'success')
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: props.selectedMisc.name,
schema: props.selectedSchema,
type: 'scheduler'
});
closeContext();
emit('reload');
};
</script> </script>

View File

@@ -1,112 +1,68 @@
<template> <template>
<BaseContextMenu <BaseContextMenu
:context-event="contextEvent" :context-event="props.contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div <div
v-if="selectedMisc === 'trigger'" v-if="props.selectedMisc === 'trigger'"
class="context-element" class="context-element"
@click="$emit('open-create-trigger-tab')" @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> <span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ t('message.createNewTrigger') }}</span>
</div> </div>
<div <div
v-if="selectedMisc === 'procedure'" v-if="['procedure', 'routine'].includes(props.selectedMisc)"
class="context-element" class="context-element"
@click="$emit('open-create-routine-tab')" @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> <span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle text-light pr-1" /> {{ t('message.createNewRoutine') }}</span>
</div> </div>
<div <div
v-if="selectedMisc === 'function'" v-if="props.selectedMisc === 'function'"
class="context-element" class="context-element"
@click="$emit('open-create-function-tab')" @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> <span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box text-light pr-1" /> {{ t('message.createNewFunction') }}</span>
</div> </div>
<div <div
v-if="selectedMisc === 'triggerFunction'" v-if="props.selectedMisc === 'triggerFunction'"
class="context-element" class="context-element"
@click="$emit('open-create-trigger-function-tab')" @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> <span class="d-flex"><i class="mdi mdi-18px mdi-cog-clockwise text-light pr-1" /> {{ t('message.createNewFunction') }}</span>
</div> </div>
<div <div
v-if="selectedMisc === 'scheduler'" v-if="props.selectedMisc === 'scheduler'"
class="context-element" class="context-element"
@click="$emit('open-create-scheduler-tab')" @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> <span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ t('message.createNewScheduler') }}</span>
</div> </div>
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import { useNotificationsStore } from '@/stores/notifications'; import { useI18n } from 'vue-i18n';
import { useWorkspacesStore } from '@/stores/workspaces'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseContextMenu from '@/components/BaseContextMenu';
import { storeToRefs } from 'pinia';
export default { const { t } = useI18n();
name: 'WorkspaceExploreBarMiscContext',
components: {
BaseContextMenu
},
props: {
contextEvent: MouseEvent,
selectedMisc: String,
selectedSchema: String
},
emits: [
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',
'open-create-trigger-function-tab',
'open-create-scheduler-tab',
'close-context'
],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
contextEvent: MouseEvent,
selectedMisc: String,
selectedSchema: String
});
const { getWorkspace, changeBreadcrumbs } = workspacesStore; const emit = defineEmits([
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',
'open-create-trigger-function-tab',
'open-create-scheduler-tab',
'close-context'
]);
return { const closeContext = () => {
addNotification, emit('close-context');
selectedWorkspace,
getWorkspace,
changeBreadcrumbs
};
},
data () {
return {
localElement: {}
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
}
},
methods: {
showDeleteModal () {
this.isDeleteModal = true;
},
hideDeleteModal () {
this.isDeleteModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
}
}
}; };
</script> </script>

View File

@@ -25,7 +25,7 @@
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="table of filteredTables" v-for="table of filteredTables"
:ref="breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name) ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name) ? 'explorebarSelected' : ''"
:key="table.name" :key="table.name"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}" :class="{'selected': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@@ -61,7 +61,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'trigger')" @contextmenu.prevent="showMiscFolderContext($event, 'trigger')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
{{ $tc('word.trigger', 2) }} {{ t('word.trigger', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
@@ -69,7 +69,7 @@
<li <li
v-for="trigger of filteredTriggers" v-for="trigger of filteredTriggers"
:key="trigger.name" :key="trigger.name"
:ref="breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@mousedown.left="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})" @mousedown.left="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})"
@@ -84,7 +84,7 @@
<div <div
v-if="trigger.enabled === false" v-if="trigger.enabled === false"
class="tooltip tooltip-left disabled-indicator" class="tooltip tooltip-left disabled-indicator"
:data-tooltip="$t('word.disabled')" :data-tooltip="t('word.disabled')"
> >
<i class="table-icon mdi mdi-pause mdi-18px mr-1" /> <i class="table-icon mdi mdi-pause mdi-18px mr-1" />
</div> </div>
@@ -100,27 +100,27 @@
<summary <summary
class="accordion-header misc-name" class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.routine}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.routine}"
@contextmenu.prevent="showMiscFolderContext($event, 'procedure')" @contextmenu.prevent="showMiscFolderContext($event, 'routine')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
{{ $tc('word.storedRoutine', 2) }} {{ t('word.storedRoutine', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="(procedure, i) of filteredProcedures" v-for="(routine, i) of filteredProcedures"
:key="`${procedure.name}-${i}`" :key="`${routine.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.routine === routine.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === routine.name}"
@mousedown.left="selectMisc({schema: database.name, misc: procedure, type: 'routine'})" @mousedown.left="selectMisc({schema: database.name, misc: routine, type: 'routine'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: procedure, type: 'routine'})" @dblclick="openMiscPermanentTab({schema: database.name, misc: routine, type: 'routine'})"
@contextmenu.prevent="showMiscContext($event, {...procedure, type: 'procedure'})" @contextmenu.prevent="showMiscContext($event, {...routine, type: 'routine'})"
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" /> <i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span v-html="highlightWord(procedure.name)" /> <span v-html="highlightWord(routine.name)" />
</a> </a>
</li> </li>
</ul> </ul>
@@ -137,7 +137,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'triggerFunction')" @contextmenu.prevent="showMiscFolderContext($event, 'triggerFunction')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-refresh mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-refresh mr-1" />
{{ $tc('word.triggerFunction', 2) }} {{ t('word.triggerFunction', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
@@ -145,7 +145,7 @@
<li <li
v-for="(func, i) of filteredTriggerFunctions" v-for="(func, i) of filteredTriggerFunctions"
:key="`${func.name}-${i}`" :key="`${func.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})" @mousedown.left="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})"
@@ -171,7 +171,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'function')" @contextmenu.prevent="showMiscFolderContext($event, 'function')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
{{ $tc('word.function', 2) }} {{ t('word.function', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
@@ -179,7 +179,7 @@
<li <li
v-for="(func, i) of filteredFunctions" v-for="(func, i) of filteredFunctions"
:key="`${func.name}-${i}`" :key="`${func.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.function === func.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.function === func.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'function'})" @mousedown.left="selectMisc({schema: database.name, misc: func, type: 'function'})"
@@ -205,7 +205,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'scheduler')" @contextmenu.prevent="showMiscFolderContext($event, 'scheduler')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
{{ $tc('word.scheduler', 2) }} {{ t('word.scheduler', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
@@ -213,7 +213,7 @@
<li <li
v-for="scheduler of filteredSchedulers" v-for="scheduler of filteredSchedulers"
:key="scheduler.name" :key="scheduler.name"
:ref="breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@mousedown.left="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})" @mousedown.left="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})"
@@ -228,7 +228,7 @@
<div <div
v-if="scheduler.enabled === false" v-if="scheduler.enabled === false"
class="tooltip tooltip-left disabled-indicator" class="tooltip tooltip-left disabled-indicator"
:data-tooltip="$t('word.disabled')" :data-tooltip="t('word.disabled')"
> >
<i class="table-icon mdi mdi-pause mdi-18px mr-1" /> <i class="table-icon mdi mdi-pause mdi-18px mr-1" />
</div> </div>
@@ -242,226 +242,235 @@
</details> </details>
</template> </template>
<script> <script setup lang="ts">
import { useSettingsStore } from '@/stores/settings'; import { computed, Prop, Ref, ref, watch } from 'vue';
import { useWorkspacesStore } from '@/stores/workspaces';
import { formatBytes } from 'common/libs/formatBytes';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useSettingsStore } from '@/stores/settings';
import { Breadcrumb, useWorkspacesStore, WorkspaceStructure } from '@/stores/workspaces';
import { formatBytes } from 'common/libs/formatBytes';
import { EventInfos, FunctionInfos, RoutineInfos, TableInfos, TriggerFunctionInfos, TriggerInfos } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceExploreBarSchema',
props: {
database: Object,
connection: Object
},
emits: [
'show-schema-context',
'show-table-context',
'show-misc-context',
'show-misc-folder-context'
],
setup () {
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { applicationTheme } = storeToRefs(settingsStore); const props = defineProps({
database: Object as Prop<WorkspaceStructure>,
connection: Object
});
const { const emit = defineEmits([
getLoadedSchemas, 'show-schema-context',
getWorkspace, 'show-table-context',
getSearchTerm, 'show-misc-context',
changeBreadcrumbs, 'show-misc-folder-context'
addLoadedSchema, ]);
newTab,
refreshSchema
} = workspacesStore;
return { const settingsStore = useSettingsStore();
applicationTheme, const workspacesStore = useWorkspacesStore();
getLoadedSchemas,
getWorkspace,
getSearchTerm,
changeBreadcrumbs,
addLoadedSchema,
newTab,
refreshSchema
};
},
data () {
return {
isLoading: false
};
},
computed: {
searchTerm () {
return this.getSearchTerm(this.connection.uid);
},
filteredTables () {
return this.database.tables.filter(table => table.name.search(this.searchTerm) >= 0);
},
filteredTriggers () {
return this.database.triggers.filter(trigger => trigger.name.search(this.searchTerm) >= 0);
},
filteredProcedures () {
return this.database.procedures.filter(procedure => procedure.name.search(this.searchTerm) >= 0);
},
filteredFunctions () {
return this.database.functions.filter(func => func.name.search(this.searchTerm) >= 0);
},
filteredTriggerFunctions () {
return this.database.triggerFunctions
? this.database.triggerFunctions.filter(func => func.name.search(this.searchTerm) >= 0)
: [];
},
filteredSchedulers () {
return this.database.schedulers.filter(scheduler => scheduler.name.search(this.searchTerm) >= 0);
},
workspace () {
return this.getWorkspace(this.connection.uid);
},
breadcrumbs () {
return this.workspace.breadcrumbs;
},
customizations () {
return this.workspace.customizations;
},
loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid);
},
maxSize () {
return this.database.tables.reduce((acc, curr) => {
if (curr.size > acc) acc = curr.size;
return acc;
}, 0);
},
totalSize () {
return this.database.tables.reduce((acc, curr) => acc + curr.size, 0);
}
},
watch: {
breadcrumbs (newVal, oldVal) {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
setTimeout(() => {
const element = this.$refs['explorebar-selected'] ? this.$refs['explorebar-selected'][0] : null;
if (element) {
const rect = element.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
const isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
if (!isVisible) { const { applicationTheme } = storeToRefs(settingsStore);
element.setAttribute('tabindex', '-1');
element.focus(); const {
element.removeAttribute('tabindex'); getLoadedSchemas,
} getWorkspace,
} getSearchTerm,
}, 50); changeBreadcrumbs,
addLoadedSchema,
newTab,
refreshSchema
} = workspacesStore;
const schemaAccordion: Ref<HTMLDetailsElement> = ref(null);
const explorebarSelected: Ref<HTMLElement[]> = ref(null);
const isLoading = ref(false);
const searchTerm = computed(() => {
return getSearchTerm(props.connection.uid);
});
const filteredTables = computed(() => {
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0);
});
const filteredTriggers = computed(() => {
return props.database.triggers.filter(trigger => trigger.name.search(searchTerm.value) >= 0);
});
const filteredProcedures = computed(() => {
return props.database.procedures.filter(procedure => procedure.name.search(searchTerm.value) >= 0);
});
const filteredFunctions = computed(() => {
return props.database.functions.filter(func => func.name.search(searchTerm.value) >= 0);
});
const filteredTriggerFunctions = computed(() => {
return props.database.triggerFunctions
? props.database.triggerFunctions.filter(func => func.name.search(searchTerm.value) >= 0)
: [];
});
const filteredSchedulers = computed(() => {
return props.database.schedulers.filter(scheduler => scheduler.name.search(searchTerm.value) >= 0);
});
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const breadcrumbs = computed(() => {
return workspace.value.breadcrumbs;
});
const customizations = computed(() => {
return workspace.value.customizations;
});
const loadedSchemas = computed(() => {
return getLoadedSchemas(props.connection.uid);
});
const maxSize = computed(() => {
return props.database.tables.reduce((acc: number, curr) => {
if (curr.size && curr.size > acc) acc = curr.size;
return acc;
}, 0);
});
watch(breadcrumbs, (newVal, oldVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
setTimeout(() => {
const element = explorebarSelected.value ? explorebarSelected.value[0] : null;
if (element) {
const rect = element.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
const isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
if (!isVisible) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
} }
} }, 50);
}, }
methods: { });
formatBytes,
async selectSchema (schema) {
if (!this.loadedSchemas.has(schema) && !this.isLoading) {
this.isLoading = true;
await this.refreshSchema({ uid: this.connection.uid, schema });
this.addLoadedSchema(schema);
this.isLoading = false;
}
},
selectTable ({ schema, table }) {
this.newTab({
uid: this.connection.uid,
elementName: table.name,
schema: this.database.name,
type: 'temp-data',
elementType: table.type
});
this.setBreadcrumbs({ schema, [table.type]: table.name }); const selectSchema = async (schema: string) => {
}, if (!loadedSchemas.value.has(schema) && !isLoading.value) {
selectMisc ({ schema, misc, type }) { isLoading.value = true;
const miscTempTabs = { setBreadcrumbs({ schema });
trigger: 'temp-trigger-props', await refreshSchema({ uid: props.connection.uid, schema });
triggerFunction: 'temp-trigger-function-props', addLoadedSchema(schema);
function: 'temp-function-props', isLoading.value = false;
routine: 'temp-routine-props',
scheduler: 'temp-scheduler-props'
};
this.newTab({
uid: this.connection.uid,
elementName: misc.name,
schema: this.database.name,
type: miscTempTabs[type],
elementType: type
});
this.setBreadcrumbs({ schema, [type]: misc.name });
},
openDataTab ({ schema, table }) {
this.newTab({ uid: this.connection.uid, elementName: table.name, schema: this.database.name, type: 'data', elementType: table.type });
this.setBreadcrumbs({ schema, [table.type]: table.name });
},
openMiscPermanentTab ({ schema, misc, type }) {
const miscTabs = {
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
scheduler: 'scheduler-props'
};
this.newTab({
uid: this.connection.uid,
elementName: misc.name,
schema: this.database.name,
type: miscTabs[type],
elementType: type
});
this.setBreadcrumbs({ schema, [type]: misc.name });
},
showSchemaContext (event, schema) {
this.$emit('show-schema-context', { event, schema });
},
showTableContext (event, table) {
this.$emit('show-table-context', { event, schema: this.database.name, table });
},
showMiscContext (event, misc) {
this.$emit('show-misc-context', { event, schema: this.database.name, misc });
},
showMiscFolderContext (event, type) {
this.$emit('show-misc-folder-context', { event, schema: this.database.name, type });
},
piePercentage (val) {
const perc = val / this.maxSize * 100;
if (this.applicationTheme === 'dark')
return { background: `conic-gradient(lime ${perc}%, white 0)` };
else
return { background: `conic-gradient(teal ${perc}%, silver 0)` };
},
setBreadcrumbs (payload) {
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
this.changeBreadcrumbs(payload);
},
highlightWord (string) {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary">$1</span>');
}
else
return string;
},
checkLoadingStatus (name, type) {
return this.workspace.loadingElements.some(el =>
el.name === name &&
el.type === type &&
el.schema === this.database.name);
}
} }
}; };
const selectTable = ({ schema, table }: { schema: string; table: TableInfos }) => {
newTab({
uid: props.connection.uid,
elementName: table.name,
schema: props.database.name,
type: 'temp-data',
elementType: table.type
});
setBreadcrumbs({ schema, [table.type]: table.name });
};
const selectMisc = ({ schema, misc, type }: { schema: string; misc: { name: string }; type: 'trigger' | 'triggerFunction' | 'function' | 'routine' | 'scheduler' }) => {
const miscTempTabs = {
trigger: 'temp-trigger-props',
triggerFunction: 'temp-trigger-function-props',
function: 'temp-function-props',
routine: 'temp-routine-props',
scheduler: 'temp-scheduler-props'
};
newTab({
uid: props.connection.uid,
elementName: misc.name,
schema: props.database.name,
type: miscTempTabs[type],
elementType: type
});
setBreadcrumbs({ schema, [type]: misc.name });
};
const openDataTab = ({ schema, table }: { schema: string; table: TableInfos }) => {
newTab({ uid: props.connection.uid, elementName: table.name, schema: props.database.name, type: 'data', elementType: table.type });
setBreadcrumbs({ schema, [table.type]: table.name });
};
const openMiscPermanentTab = ({ schema, misc, type }: { schema: string; misc: { name: string }; type: 'trigger' | 'triggerFunction' | 'function' | 'routine' | 'scheduler' }) => {
const miscTabs = {
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
scheduler: 'scheduler-props'
};
newTab({
uid: props.connection.uid,
elementName: misc.name,
schema: props.database.name,
type: miscTabs[type],
elementType: type
});
setBreadcrumbs({ schema, [type]: misc.name });
};
const showSchemaContext = (event: MouseEvent, schema: string) => {
emit('show-schema-context', { event, schema });
};
const showTableContext = (event: MouseEvent, table: TableInfos) => {
emit('show-table-context', { event, schema: props.database.name, table });
};
const showMiscContext = (event: MouseEvent, misc: TriggerInfos | TriggerFunctionInfos | RoutineInfos | FunctionInfos | EventInfos) => {
emit('show-misc-context', { event, schema: props.database.name, misc });
};
const showMiscFolderContext = (event: MouseEvent, type: string) => {
emit('show-misc-folder-context', { event, schema: props.database.name, type });
};
const piePercentage = (val: number) => {
const perc = val / maxSize.value * 100;
if (applicationTheme.value === 'dark')
return { background: `conic-gradient(lime ${perc}%, white 0)` };
else
return { background: `conic-gradient(teal ${perc}%, silver 0)` };
};
const setBreadcrumbs = (payload: Breadcrumb) => {
if (breadcrumbs.value.schema === payload.schema && breadcrumbs.value.table === payload.table) return;
changeBreadcrumbs(payload);
};
const highlightWord = (string: string) => {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (searchTerm.value) {
const regexp = new RegExp(`(${searchTerm.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary">$1</span>');
}
else
return string;
};
const checkLoadingStatus = (name: string, type: string) => {
return workspace.value.loadingElements.some(el =>
el.name === name &&
el.type === type &&
el.schema === props.database.name);
};
defineExpose({ selectSchema, schemaAccordion });
</script> </script>
<style lang="scss"> <style lang="scss">

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