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

Compare commits

...

164 Commits

Author SHA1 Message Date
cc941dfc04 chore(release): 0.5.12 2022-07-26 14:59:06 +02:00
1d151e9349 fix: error on schema export 2022-07-26 13:33:57 +02:00
addd9fba28 build(deps): bump ace-builds from 1.4.14 to 1.8.1 2022-07-25 15:25:58 +02:00
a00c19d300 fix: prevent ctrl+a in console 2022-07-25 15:20:15 +02:00
9551afbd2d feat: ability to copy multiple selected rows 2022-07-25 14:56:00 +02:00
1ead76c028 fix: missing defaults on insert row window 2022-07-25 13:09:41 +02:00
d3da15aa13 feat: copy row as SQL INSERT 2022-07-25 12:42:22 +02:00
f3b5de38c4 feat: export table content as SQL INSERT 2022-07-25 12:19:58 +02:00
Askar Kanturin
d4b6d2e9d1 changed readme 2022-07-23 10:20:36 +02:00
Askar Kanturin
e2c106e4e0 fixed typo in readme
i'm no native speaker, but i feel this is a typo
2022-07-23 10:20:36 +02:00
eb60899e6e fix(MySQL): missing quoted identifier for column names in table filter, closes #380 2022-07-22 10:08:33 +02:00
1d367d468d Merge branch 'master' of https://github.com/antares-sql/antares 2022-07-21 11:01:13 +02:00
8ecaedbf6c fix: disable ctrl+alt+(left/right) shortcut on linux 2022-07-21 11:01:10 +02:00
dependabot[bot]
dd1eebd4ec build(deps): bump terser from 5.13.1 to 5.14.2
Bumps [terser](https://github.com/terser/terser) from 5.13.1 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 08:43:24 +02:00
8c83b3f144 fix: missing table on insert new records on session restored tabs 2022-07-20 10:41:44 +02:00
985e5d3527 feat: context menu option to duplicate a table row 2022-07-19 17:48:51 +02:00
78902639eb feat: execute selected query 2022-07-19 15:02:17 +02:00
cb038b374a fix: issue with logger on import/export 2022-07-19 10:10:24 +02:00
eafdb1cc3d chore(release): 0.5.11 2022-07-19 08:14:47 +02:00
91e0630513 fix: unable to edit table fields content on tables with datetime fields 2022-07-19 08:08:51 +02:00
bf768c3800 fix: filter persists switching temporary table tabs 2022-07-18 18:29:12 +02:00
0b1aa3dd29 fix: console events disabled in production 2022-07-18 17:21:34 +02:00
ec75f9546a chore(release): 0.5.10 2022-07-18 14:44:30 +02:00
9bc9adb7cf fix: exception on QueryEditor with null modelValue 2022-07-18 14:43:38 +02:00
b4d14d98db refactor: improved console with light theme 2022-07-18 11:58:22 +02:00
c21bd6075c feat: context menu to copy queries from console 2022-07-18 11:58:22 +02:00
44647f5b55 feat: open/close console on single connection 2022-07-18 11:58:22 +02:00
3f9e6d85ca perf: improved resize of text editor resizing console height 2022-07-18 11:58:22 +02:00
6a6f43a718 feat: initial console implementation 2022-07-18 11:58:22 +02:00
f12a04b052 feat: ipc event channel to send logs to renderer 2022-07-18 11:58:22 +02:00
abf829867e feat: Ctrl+PgUp & Ctrl+PgDn to navigate between tabs 2022-07-14 19:30:34 +02:00
b71f04e5aa feat: field names suggestion for tables in the query 2022-07-14 16:09:04 +02:00
7725fafe85 fix: unable to delete by select all in left bar search, closes #368 2022-07-13 18:50:16 +02:00
ed3d35f131 fix(Linux): ctrl+space shortcut not working 2022-07-13 18:25:42 +02:00
e0946f04f7 fix: unable to update data on tables missing primary or unique key 2022-07-13 09:30:30 +02:00
0891e7be8c refactor: disabled autofocus for scheduler timing modal 2022-07-11 11:35:30 +02:00
f312cf5f85 perf(UI): improved visibility of explore bar tooltips 2022-07-11 09:56:33 +02:00
05e0d310ec Merge pull request #363 from goYou/master
feat: Update zh-CN.ts
2022-07-10 09:13:35 +02:00
goYou
1e0b2b4cae Update zh-CN.ts 2022-07-10 14:49:30 +08:00
5ee728cfe4 Merge pull request #361 from antares-sql/dependabot/npm_and_yarn/moment-2.29.4
build(deps): bump moment from 2.29.3 to 2.29.4
2022-07-09 12:41:20 +02:00
a91fa8ff54 fix: fields content language detection not working properly 2022-07-09 12:39:44 +02:00
dependabot[bot]
2c13433900 build(deps): bump moment from 2.29.3 to 2.29.4
Bumps [moment](https://github.com/moment/moment) from 2.29.3 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.3...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-09 10:38:14 +00:00
dcc2a4c51c refactor: replaced @vscode/vscode-languagedetection with custom language detection 2022-07-09 12:37:30 +02:00
7537dff401 chore(release): 0.5.9 2022-07-06 14:42:32 +02:00
853ee1f572 revert: revert to better-sqlite 7.5.1 due build issues 2022-07-06 10:43:32 +02:00
cf9c7c600a fix: error on export schema 2022-07-06 10:26:24 +02:00
73b88cad9e Merge branch 'master' of https://github.com/antares-sql/antares 2022-07-06 09:37:21 +02:00
d2eb31a63d perf(UI): improved focus visibility for buttons 2022-07-06 09:37:18 +02:00
d6b36b1f80 Merge pull request #353 from antares-sql/dependabot/npm_and_yarn/better-sqlite3-7.5.3
build(deps): bump better-sqlite3 from 7.5.1 to 7.5.3
2022-07-05 18:20:56 +02:00
dependabot[bot]
d66b932683 build(deps): bump better-sqlite3 from 7.5.1 to 7.5.3
Bumps [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) from 7.5.1 to 7.5.3.
- [Release notes](https://github.com/WiseLibs/better-sqlite3/releases)
- [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v7.5.1...v7.5.3)

---
updated-dependencies:
- dependency-name: better-sqlite3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-05 06:59:47 +00:00
20f5497034 Merge pull request #341 from antares-sql/dependabot/npm_and_yarn/vue-3.2.37
build(deps): bump vue from 3.2.33 to 3.2.37
2022-07-05 08:54:40 +02:00
d4ed886489 Merge pull request #339 from antares-sql/dependabot/npm_and_yarn/mdi/font-6.9.96
build(deps): bump @mdi/font from 6.1.95 to 6.9.96
2022-07-05 08:54:30 +02:00
dependabot[bot]
aaa14f112a build(deps): bump vue from 3.2.33 to 3.2.37
Bumps [vue](https://github.com/vuejs/core) from 3.2.33 to 3.2.37.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.2.33...v3.2.37)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-04 15:41:08 +00:00
dependabot[bot]
e0f3ff6939 build(deps): bump @mdi/font from 6.1.95 to 6.9.96
Bumps [@mdi/font](https://github.com/Templarian/MaterialDesign-Webfont) from 6.1.95 to 6.9.96.
- [Release notes](https://github.com/Templarian/MaterialDesign-Webfont/releases)
- [Commits](https://github.com/Templarian/MaterialDesign-Webfont/compare/v6.1.95...v6.9.96)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-04 15:41:01 +00:00
72a328785c Merge pull request #333 from antares-sql/new-connection-management
New connections management
2022-07-04 17:39:56 +02:00
4c2a1998a9 Merge branch 'master' of https://github.com/antares-sql/antares into new-connection-management 2022-07-04 17:25:37 +02:00
8e705706ae feat: ability to pin/unpin and delete connections from the "all connections" modal 2022-07-04 17:03:24 +02:00
56b0a4815c feat: option to disable scratchpad 2022-07-04 15:10:40 +02:00
a9a4344a71 feat: ctrl/cmd+space to open all connections modal 2022-07-04 12:42:33 +02:00
ec5ab73b19 feat: search form in all connections modal 2022-07-04 12:27:04 +02:00
71a5b5c828 fix: missing option for untrusted ssl connection on connections edit panel 2022-07-03 22:07:28 +02:00
4519829aa2 chore(release): 0.5.8 2022-07-02 15:32:02 +02:00
a703dcc53e feat: modal with all connections 2022-07-02 15:30:36 +02:00
36e98e0742 feat: connections sorted by last usage by default and option to pin them 2022-06-30 18:20:14 +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
f632a0f189 initial implementation of new feature 2022-06-29 19:05:45 +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
201 changed files with 46393 additions and 13226 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,222 @@
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.12](https://github.com/antares-sql/antares/compare/v0.5.11...v0.5.12) (2022-07-26)
### Features
* ability to copy multiple selected rows ([9551afb](https://github.com/antares-sql/antares/commit/9551afbd2d7e525c81f28e98e788b92609ce9de4))
* context menu option to duplicate a table row ([985e5d3](https://github.com/antares-sql/antares/commit/985e5d352793d1b3e1981d004b6f494bfbb049bf))
* copy row as SQL INSERT ([d3da15a](https://github.com/antares-sql/antares/commit/d3da15aa1377dcba73927047563f1d0c2d1284ca))
* execute selected query ([7890263](https://github.com/antares-sql/antares/commit/78902639ebb29a8c53f8aa0d2045c74e0646febc))
* export table content as SQL INSERT ([f3b5de3](https://github.com/antares-sql/antares/commit/f3b5de38c4abfd2c1d738e179fc22e6c8b6f9080))
### Bug Fixes
* disable ctrl+alt+(left/right) shortcut on linux ([8ecaedb](https://github.com/antares-sql/antares/commit/8ecaedbf6c2fc0dc56ff2177a87dd6ede74bdd22))
* error on schema export ([1d151e9](https://github.com/antares-sql/antares/commit/1d151e9349fd97576ccd8ef7f88ca789a1f28b65))
* issue with logger on import/export ([cb038b3](https://github.com/antares-sql/antares/commit/cb038b374a4fe85ad569e42eee7af123c925e775))
* missing defaults on insert row window ([1ead76c](https://github.com/antares-sql/antares/commit/1ead76c02889f48bd91cae702820b082ca2ff54b))
* missing table on insert new records on session restored tabs ([8c83b3f](https://github.com/antares-sql/antares/commit/8c83b3f1447354ec63b2a308db05ad4d54659aa7))
* **MySQL:** missing quoted identifier for column names in table filter, closes [#380](https://github.com/antares-sql/antares/issues/380) ([eb60899](https://github.com/antares-sql/antares/commit/eb60899e6e17879c79a7ee7108061e9aca8596f7))
* prevent ctrl+a in console ([a00c19d](https://github.com/antares-sql/antares/commit/a00c19d3003cd43d3ee6e3132728122bb2b24c97))
### [0.5.11](https://github.com/antares-sql/antares/compare/v0.5.10...v0.5.11) (2022-07-19)
### Bug Fixes
* console events disabled in production ([0b1aa3d](https://github.com/antares-sql/antares/commit/0b1aa3dd299db641df3d4c56c7ee56a187fc3ab3))
* filter persists switching temporary table tabs ([bf768c3](https://github.com/antares-sql/antares/commit/bf768c380087b65604b5b571a9858a7f07bd681d))
* unable to edit table fields content on tables with datetime fields ([91e0630](https://github.com/antares-sql/antares/commit/91e06305133c97ea02dcfdc4e739a4b0a7e7049d))
### [0.5.10](https://github.com/antares-sql/antares/compare/v0.5.9...v0.5.10) (2022-07-18)
### Features
* context menu to copy queries from console ([c21bd60](https://github.com/antares-sql/antares/commit/c21bd6075c1203607c05e45b76233d57e3008190))
* Ctrl+PgUp & Ctrl+PgDn to navigate between tabs ([abf8298](https://github.com/antares-sql/antares/commit/abf829867e567354e534cff3e02a6d43f4c7a262))
* field names suggestion for tables in the query ([b71f04e](https://github.com/antares-sql/antares/commit/b71f04e5aa3c37eaa160dfbc76d1b84789e3543e))
* initial console implementation ([6a6f43a](https://github.com/antares-sql/antares/commit/6a6f43a718561e0abd2cb89048b7fe45d08736ae))
* ipc event channel to send logs to renderer ([f12a04b](https://github.com/antares-sql/antares/commit/f12a04b0524f1172334c89afeb27675c19ff68d2))
* open/close console on single connection ([44647f5](https://github.com/antares-sql/antares/commit/44647f5b5508965bf5a7264add89e175c725e877))
### Bug Fixes
* exception on QueryEditor with null modelValue ([9bc9adb](https://github.com/antares-sql/antares/commit/9bc9adb7cff19b86a99491d968485a4cd7b47b99))
* fields content language detection not working properly ([a91fa8f](https://github.com/antares-sql/antares/commit/a91fa8ff54bbf1f8475666efd3a268a3a4f07f0c))
* **Linux:** ctrl+space shortcut not working ([ed3d35f](https://github.com/antares-sql/antares/commit/ed3d35f1319a1e2edcb8104f2045a71b9e9754a2))
* unable to delete by select all in left bar search, closes [#368](https://github.com/antares-sql/antares/issues/368) ([7725faf](https://github.com/antares-sql/antares/commit/7725fafe852479720fa619ced0970f2fa0099191))
* unable to update data on tables missing primary or unique key ([e0946f0](https://github.com/antares-sql/antares/commit/e0946f04f792d25c187ea56d4714bdacc016ada3))
### Improvements
* improved resize of text editor resizing console height ([3f9e6d8](https://github.com/antares-sql/antares/commit/3f9e6d85ca445eea1028effa32418eee4980f87d))
* **UI:** improved visibility of explore bar tooltips ([f312cf5](https://github.com/antares-sql/antares/commit/f312cf5f855deddd562c26d1835f78d16499b93b))
### [0.5.9](https://github.com/antares-sql/antares/compare/v0.5.8...v0.5.9) (2022-07-06)
### Features
* ability to pin/unpin and delete connections from the "all connections" modal ([8e70570](https://github.com/antares-sql/antares/commit/8e705706aecc5c9790329e63e61a1c02fa5d0342))
* connections sorted by last usage by default and option to pin them ([36e98e0](https://github.com/antares-sql/antares/commit/36e98e0742657e25df7768aa5b3b7cb350df5509))
* ctrl/cmd+space to open all connections modal ([a9a4344](https://github.com/antares-sql/antares/commit/a9a4344a71cc0f8f156b839733f6ddc200a26268))
* modal with all connections ([a703dcc](https://github.com/antares-sql/antares/commit/a703dcc53eb920117bc346a3c21f0c729c0ad96d))
* option to disable scratchpad ([56b0a48](https://github.com/antares-sql/antares/commit/56b0a4815c6f54eef164d849f6ca25af1e142b16))
* search form in all connections modal ([ec5ab73](https://github.com/antares-sql/antares/commit/ec5ab73b19d99e9971ae87e5f0a8d1bd1c34ef00))
### Bug Fixes
* error on export schema ([cf9c7c6](https://github.com/antares-sql/antares/commit/cf9c7c600aa915cef1ec3777866badb7ab1312ee))
* missing option for untrusted ssl connection on connections edit panel ([71a5b5c](https://github.com/antares-sql/antares/commit/71a5b5c8285fb777c43e7f6516006bfe9f52591c))
### Improvements
* **UI:** improved focus visibility for buttons ([d2eb31a](https://github.com/antares-sql/antares/commit/d2eb31a63d612323f8738eded1e1ce7b23554001))
### [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

@@ -13,7 +13,7 @@ Antares is an SQL client based on [Electron.js](https://github.com/electron/elec
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions. Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite. **At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite.
At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it. However, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible. We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible.
🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest). 🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest).
@@ -40,20 +40,20 @@ We are actively working on it, hoping to provide new cool features, improvements
Why are we developing an SQL client when there are a lot of them on the market? Why are we developing an SQL client when there are a lot of them on the market?
The main goal is to develop a **forever 100% free (without paid premium feature)**, full featured, as possible community driven, cross platform and open source alternative, empowered by JavaScript ecosystem. The main goal is to develop a **forever 100% free (without paid premium feature)**, full featured, as possible community driven, cross platform and open source alternative, empowered by JavaScript ecosystem.
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenu; productivity comes first. A modern application created with minimalism and simplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenues; productivity comes first.
## Installation ## Installation
Based on your operating system you can have one or more distribution formats to choose based on your preferences. Based on your operating system you can have one or more distribution formats to choose based on your preferences.
Since Antares SQL is a free software we haven't a budget to spend in annual licenses or certificates. This can result that on some platforms you need some additional passages to install this app. Since Antares SQL is a free software we don't have a budget to spend on annual licenses or certificates. This can result that on some platforms you might need to put in some additional work to install this app.
### Linux ### Linux
On Linux you can simply download and run `.AppImage` distributions, install from Snap Store or from AUR. On Linux you can simply download and run the `.AppImage` distribution, install from Snap Store or from AUR.
### Windows ### Windows
On Windows you can choose between Microsoft Store and download `.exe` distribution. The latter lacks of a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt. On Windows you can choose between downloading the app from Microsoft Store or downloading the `.exe` from our [website](https://antares-sql.app/download.html) or [this github repo](https://github.com/Fabio286/antares/releases/latest). Distributions that are not from Microsoft Store are not signed with a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt.
### MacOS ### MacOS
@@ -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

31098
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.12",
"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"
}, },
@@ -111,19 +117,19 @@
"dependencies": { "dependencies": {
"@electron/remote": "~2.0.1", "@electron/remote": "~2.0.1",
"@faker-js/faker": "~6.1.2", "@faker-js/faker": "~6.1.2",
"@mdi/font": "~6.1.95", "@mdi/font": "~6.9.96",
"@turf/helpers": "~6.5.0", "@turf/helpers": "~6.5.0",
"@vscode/vscode-languagedetection": "~1.0.21", "@vueuse/core": "~8.7.5",
"ace-builds": "~1.4.13", "ace-builds": "~1.8.1",
"better-sqlite3": "~7.5.0", "better-sqlite3": "~7.5.1",
"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",
"marked": "~4.0.0", "marked": "~4.0.0",
"moment": "~2.29.1", "moment": "~2.29.4",
"mysql2": "~2.3.2", "mysql2": "~2.3.2",
"pg": "~8.7.1", "pg": "~8.7.1",
"pg-query-stream": "~4.2.3", "pg-query-stream": "~4.2.3",
@@ -134,7 +140,7 @@
"sql-formatter": "~4.0.2", "sql-formatter": "~4.0.2",
"ssh2-promise": "~1.0.2", "ssh2-promise": "~1.0.2",
"v-mask": "~2.3.0", "v-mask": "~2.3.0",
"vue": "~3.2.33", "vue": "~3.2.37",
"vue-i18n": "~9.1.9", "vue-i18n": "~9.1.9",
"vuedraggable": "~4.1.0" "vuedraggable": "~4.1.0"
}, },
@@ -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,
@@ -24,9 +25,10 @@ module.exports = {
functions: true, functions: true,
schedulers: true, schedulers: true,
// Settings // Settings
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,11 +14,18 @@ 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
*/ */
export interface ClientParams { export interface ClientParams {
client: ClientCode; client: ClientCode;
uid?: string;
params: params:
mysql.ConnectionOptions & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean} mysql.ConnectionOptions & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean}
| pg.ClientConfig & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean} | pg.ClientConfig & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean}
@@ -67,12 +74,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 +111,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 +122,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 +144,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 +157,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 +195,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 +211,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 +239,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 +308,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 +317,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 +377,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,13 @@ export interface TableParams {
export interface ExportOptions { export interface ExportOptions {
schema: string; schema: string;
includes: { tables: {
functions: boolean; table: string;
views: boolean; includeStructure: boolean;
triggers: boolean; includeContent: boolean;
routines: boolean; includeDropStatement: boolean;
schedulers: boolean; }[];
}; includes: {[key: string]: 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

@@ -0,0 +1,192 @@
function isJSON (str: string) {
try {
if (!['{', '['].includes(str.trim()[0]))
return false;
JSON.parse(str);
return true;
}
catch (_) {
return false;
}
}
function isHTML (str: string) {
const tags = [
'a',
'abbr',
'address',
'area',
'article',
'aside',
'audio',
'b',
'base',
'bdi',
'bdo',
'blockquote',
'body',
'br',
'button',
'canvas',
'caption',
'cite',
'code',
'col',
'colgroup',
'data',
'datalist',
'dd',
'del',
'details',
'dfn',
'dialog',
'div',
'dl',
'dt',
'em',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'i',
'iframe',
'img',
'input',
'ins',
'kbd',
'label',
'legend',
'li',
'link',
'main',
'map',
'mark',
'math',
'menu',
'menuitem',
'meta',
'meter',
'nav',
'noscript',
'object',
'ol',
'optgroup',
'option',
'output',
'p',
'param',
'picture',
'pre',
'progress',
'q',
'rb',
'rp',
'rt',
'rtc',
'ruby',
's',
'samp',
'script',
'section',
'select',
'slot',
'small',
'source',
'span',
'strong',
'style',
'sub',
'summary',
'sup',
'svg',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'track',
'u',
'ul',
'var',
'video',
'wbr'
];
const doc = new DOMParser().parseFromString(str, 'text/html');
const lowerStr = str.toLowerCase();
if (Array.from(doc.body.childNodes).some(node => node.nodeType === 1))
return tags.some((tag) => lowerStr.includes(`<${tag}>`));
return false;
}
function isSVG (str: string) {
const doc = new DOMParser().parseFromString(str, 'text/xml');
const lowerStr = str.toLowerCase();
const errorNode = doc.querySelector('parsererror');
if (!errorNode)
return lowerStr.includes('<svg');
return false;
}
function isXML (str: string) {
const doc = new DOMParser().parseFromString(str, 'text/xml');
const errorNode = doc.querySelector('parsererror');
return !errorNode;
}
function isMD (str: string) {
const mdChecks = [
'# ',
'`',
'- ',
'+ ',
'* ',
'1. ',
'**',
'__',
'~~',
'>> ',
'](http',
'![',
'[ ]',
'[x]'
];
return mdChecks.some((tag) => str.includes(tag));
}
export function langDetector (str: string) {
if (!str.trim().length)
return 'text';
if (isJSON(str))
return 'json';
if (isHTML(str))
return 'html';
if (isSVG(str))
return 'svg';
if (isXML(str))
return 'xml';
if (isMD(str))
return 'markdown';
return 'text';
}

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

@@ -1,20 +0,0 @@
/* eslint-disable no-useless-escape */
// eslint-disable-next-line no-control-regex
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
const regex = new RegExp(pattern);
/**
* Escapes a string
*
* @param {String} string
* @returns {String}
*/
function sqlEscaper (string) {
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];
return r[m.indexOf(char)] || char;
});
}
export { sqlEscaper };

162
src/common/libs/sqlUtils.ts Normal file
View File

@@ -0,0 +1,162 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-useless-escape */
import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
import customizations from '../customizations';
import { ClientCode } from '../interfaces/antares';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary, { HexChar } from './hexToBinary';
import { getArrayDepth } from './getArrayDepth';
/**
* Escapes a string fo SQL use
*
* @param { String } string
* @returns { String } Escaped string
*/
export const sqlEscaper = (string: string): string => {
// eslint-disable-next-line no-control-regex
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
const regex = new RegExp(pattern);
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];
return r[m.indexOf(char)] || char;
});
};
export const objectToGeoJSON = (val: any) => {
if (Array.isArray(val)) {
if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(val.map(arr => arr.reduce((acc: any, curr: any) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
};
export const escapeAndQuote = (val: string, client: ClientCode) => {
const { stringsWrapper: sw } = customizations[client];
// eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP: {[key: string]: string} = {
'\0': '\\0',
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\r': '\\r',
'\x1a': '\\Z',
'"': '\\"',
'\'': '\\\'',
'\\': '\\\\'
};
let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0;
let escapedVal = '';
let match;
while ((match = CHARS_TO_ESCAPE.exec(val))) {
escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]];
chunkIndex = CHARS_TO_ESCAPE.lastIndex;
}
if (chunkIndex === 0)
return `${sw}${val}${sw}`;
if (chunkIndex < val.length)
return `${sw}${escapedVal + val.slice(chunkIndex)}${sw}`;
return `${sw}${escapedVal}${sw}`;
};
export const valueToSqlString = (args: {
val: any;
client: ClientCode;
field: {type: string; datePrecision: number};
}): string => {
let parsedValue;
const { val, client, field } = args;
const { stringsWrapper: sw } = customizations[client];
if (val === null)
parsedValue = 'NULL';
else if (DATE.includes(field.type)) {
parsedValue = moment(val).isValid()
? escapeAndQuote(moment(val).format('YYYY-MM-DD'), client)
: val;
}
else if (DATETIME.includes(field.type)) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
parsedValue = moment(val).isValid()
? escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`), client)
: escapeAndQuote(val, client);
}
else if ('isArray' in field) {
let localVal;
if (Array.isArray(val))
localVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
else
localVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : '';
parsedValue = `'${localVal}'`;
}
else if (TEXT_SEARCH.includes(field.type))
parsedValue = `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(field.type))
parsedValue = `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(field.type)) {
if (['mysql', 'maria'].includes(client))
parsedValue = `X'${val.toString('hex').toUpperCase()}'`;
else if (client === 'pg')
parsedValue = `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
}
else if (NUMBER.includes(field.type))
parsedValue = val;
else if (FLOAT.includes(field.type))
parsedValue = parseFloat(val);
else if (SPATIAL.includes(field.type)) {
let geoJson;
if (IS_MULTI_SPATIAL.includes(field.type)) {
const features = [];
for (const element of val)
features.push(objectToGeoJSON(element));
geoJson = {
type: 'FeatureCollection',
features
};
}
else
geoJson = objectToGeoJSON(val);
parsedValue = `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
}
else if (val === '') parsedValue = `${sw}${sw}`;
else {
parsedValue = typeof val === 'string'
? escapeAndQuote(val, client)
: typeof val === 'object'
? escapeAndQuote(JSON.stringify(val), client)
: val;
}
return parsedValue;
};
export const jsonToSqlInsert = (args: {
json: { [key: string]: any};
client: ClientCode;
fields: { [key: string]: {type: string; datePrecision: number}};
table: string;
}) => {
const { client, json, fields, table } = args;
const { elementsWrapper: ew } = customizations[client];
const fieldNames = Object.keys(json).map(key => `${ew}${key}${ew}`);
const values = Object.keys(json).map(key => (
valueToSqlString({ val: json[key], client, field: fields[key] })
));
return `INSERT INTO ${ew}${table}${ew} (${fieldNames.join(', ')}) VALUES (${values.join(', ')});`;
};

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

69
src/common/shortcuts.ts Normal file
View File

@@ -0,0 +1,69 @@
interface ShortcutRecord {
event: string;
keys: Electron.Accelerator[];
description: string;
os: NodeJS.Platform[];
}
const shortcuts: ShortcutRecord[] = [
{
event: 'open-new-tab',
keys: ['CommandOrControl+T'],
description: 'Open a new query tab',
os: ['darwin', 'linux', 'win32']
},
{
event: 'close-tab',
keys: ['CommandOrControl+W'],
description: 'Close tab',
os: ['darwin', 'linux', 'win32']
},
{
event: 'next-tab',
keys: ['Alt+CommandOrControl+Right', 'CommandOrControl+PageDown'],
description: 'Next tab',
os: ['darwin', 'win32']
},
{
event: 'prev-tab',
keys: ['Alt+CommandOrControl+Left', 'CommandOrControl+PageUp'],
description: 'Previous tab',
os: ['darwin', 'win32']
},
{
event: 'next-tab',
keys: ['CommandOrControl+PageDown'],
description: 'Next tab',
os: ['linux']
},
{
event: 'prev-tab',
keys: ['CommandOrControl+PageUp'],
description: 'Previous tab',
os: ['linux']
},
{
event: 'open-connections-modal',
keys: ['Shift+CommandOrControl+Space'],
description: 'Show all connections',
os: ['darwin', 'linux', 'win32']
},
{
event: 'toggle-console',
keys: ['CommandOrControl+F12', 'CommandOrControl+`'],
description: 'Toggle console',
os: ['darwin', 'linux', 'win32']
}
];
for (let i = 1; i <= 9; i++) {
shortcuts.push(
{
event: `select-tab-${i}`,
keys: [`CommandOrControl+${i}`],
description: `Select tab number ${i}`,
os: ['darwin', 'linux', 'win32']
});
}
export { shortcuts, ShortcutRecord };

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';
@@ -55,6 +55,7 @@ export default (connections: {[key: string]: antares.Client}) => {
try { try {
const connection = await ClientsFactory.getClient({ const connection = await ClientsFactory.getClient({
uid: conn.uid,
client: conn.client, client: conn.client,
params params
}); });
@@ -128,6 +129,7 @@ export default (connections: {[key: string]: antares.Client}) => {
try { try {
const connection = ClientsFactory.getClient({ const connection = ClientsFactory.getClient({
uid: conn.uid,
client: conn.client, client: conn.client,
params, params,
poolSize: 5 poolSize: 5

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/sqlUtils';
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

@@ -3,10 +3,14 @@ import mysql from 'mysql2/promise';
import * as pg from 'pg'; import * as pg from 'pg';
import SSH2Promise from 'ssh2-promise'; import SSH2Promise from 'ssh2-promise';
const queryLogger = (sql: string) => { const queryLogger = ({ sql, cUid }: {sql: string; cUid: string}) => {
// Remove comments, newlines and multiple spaces // Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' '); const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql); if (process.type !== undefined) {
const mainWindow = require('electron').webContents.fromId(1);
mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() });
}
if (process.env.NODE_ENV === 'development') console.log(escapedSql);
}; };
/** /**
@@ -14,15 +18,17 @@ const queryLogger = (sql: string) => {
*/ */
export class AntaresCore { export class AntaresCore {
_client: antares.ClientCode; _client: antares.ClientCode;
protected _cUid: string
protected _params: mysql.ConnectionOptions | pg.ClientConfig | { databasePath: string; readonly: boolean}; protected _params: mysql.ConnectionOptions | pg.ClientConfig | { databasePath: string; readonly: boolean};
protected _poolSize: number; protected _poolSize: number;
protected _ssh?: SSH2Promise; protected _ssh?: SSH2Promise;
protected _logger: (sql: string) => void; protected _logger: (args: {sql: string; cUid: string}) => void;
protected _queryDefaults: antares.QueryBuilderObject; protected _queryDefaults: antares.QueryBuilderObject;
protected _query: antares.QueryBuilderObject; protected _query: antares.QueryBuilderObject;
constructor (args: antares.ClientParams) { constructor (args: antares.ClientParams) {
this._client = args.client; this._client = args.client;
this._cUid = args.uid;
this._params = args.params; this._params = args.params;
this._poolSize = args.poolSize || undefined; this._poolSize = args.poolSize || undefined;
this._logger = args.logger || queryLogger; this._logger = args.logger || queryLogger;

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;
}; };
}); });
} }
@@ -1511,7 +1536,7 @@ export class MySQLClient extends AntaresCore {
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
if (process.env.NODE_ENV === 'development') this._logger(sql); this._logger({ cUid: this._cUid, sql });
args = { args = {
nest: false, nest: false,

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) {
@@ -1310,7 +1314,7 @@ export class PostgreSQLClient extends AntaresCore {
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
if (process.env.NODE_ENV === 'development') this._logger(sql); this._logger({ cUid: this._cUid, sql });
args = { args = {
nest: false, nest: false,

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 {
@@ -586,7 +586,7 @@ export class SQLiteClient extends AntaresCore {
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder this._logger({ cUid: this._cUid, sql });// TODO: replace BLOB content with a placeholder
args = { args = {
nest: false, nest: false,

View File

@@ -1,12 +1,8 @@
import * as exporter from 'common/interfaces/exporter'; 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 hexToBinary from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth';
import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
import { MySQLClient } from '../../clients/MySQLClient'; import { MySQLClient } from '../../clients/MySQLClient';
import { valueToSqlString } from 'common/libs/sqlUtils';
export default class MysqlExporter extends SqlExporter { export default class MysqlExporter extends SqlExporter {
protected _client: MySQLClient; protected _client: MySQLClient;
@@ -122,54 +118,7 @@ ${footer}
const column = notGeneratedColumns[i]; const column = notGeneratedColumns[i];
const val = row[column.name]; const val = row[column.name];
if (val === null) sqlInsertString += 'NULL'; sqlInsertString += valueToSqlString({ val, client: 'mysql', field: column });
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (SPATIAL.includes(column.type)) {
let geoJson;
if (IS_MULTI_SPATIAL.includes(column.type)) {
const features = [];
for (const element of val)
features.push(this._getGeoJSON(element));
geoJson = {
type: 'FeatureCollection',
features
};
}
else
geoJson = this._getGeoJSON(val);
sqlInsertString += `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`;
}
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== notGeneratedColumns.length - 1) if (parseInt(i) !== notGeneratedColumns.length - 1)
sqlInsertString += ', '; sqlInsertString += ', ';
@@ -435,17 +384,4 @@ CREATE TABLE \`${view.Name}\`(
return `'${escapedVal}'`; return `'${escapedVal}'`;
} }
/* eslint-disable @typescript-eslint/no-explicit-any */
_getGeoJSON (val: any) {
if (Array.isArray(val)) {
if (getArrayDepth(val) === 1)
return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(val.map(arr => arr.reduce((acc: any, curr: any) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([val.x, val.y]);
}
/* eslint-enable @typescript-eslint/no-explicit-any */
} }

View File

@@ -1,13 +1,11 @@
import * as antares from 'common/interfaces/antares'; 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 hexToBinary from 'common/libs/hexToBinary';
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
import * as QueryStream from 'pg-query-stream'; import * as QueryStream from 'pg-query-stream';
import { PostgreSQLClient } from '../../clients/PostgreSQLClient'; import { PostgreSQLClient } from '../../clients/PostgreSQLClient';
import { valueToSqlString } from 'common/libs/sqlUtils';
export default class PostgreSQLExporter extends SqlExporter { export default class PostgreSQLExporter extends SqlExporter {
constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) { constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) {
@@ -223,47 +221,7 @@ SET row_security = off;\n\n\n`;
const column = columns[i]; const column = columns[i];
const val = row[column.name]; const val = row[column.name];
if (val === null) sqlInsertString += 'NULL'; sqlInsertString += valueToSqlString({ val, client: 'pg', field: column });
else if (DATE.includes(column.type)) {
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format('YYYY-MM-DD'))
: val;
}
else if (DATETIME.includes(column.type)) {
let datePrecision = '';
for (let i = 0; i < column.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
sqlInsertString += moment(val).isValid()
? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`))
: this.escapeAndQuote(val);
}
else if ('isArray' in column) {
let parsedVal;
if (Array.isArray(val))
parsedVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
else
parsedVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : '';
sqlInsertString += `'${parsedVal}'`;
}
else if (TEXT_SEARCH.includes(column.type))
sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`;
else if (BLOB.includes(column.type))
sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
else if (NUMBER.includes(column.type))
sqlInsertString += val;
else if (FLOAT.includes(column.type))
sqlInsertString += parseFloat(val);
else if (val === '') sqlInsertString += '\'\'';
else {
sqlInsertString += typeof val === 'string'
? this.escapeAndQuote(val)
: typeof val === 'object'
? this.escapeAndQuote(JSON.stringify(val))
: val;
}
if (parseInt(i) !== columns.length - 1) if (parseInt(i) !== columns.length - 1)
sqlInsertString += ', '; sqlInsertString += ', ';

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,15 +1,20 @@
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron'; import { app, BrowserWindow, globalShortcut, 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';
import * as remoteMain from '@electron/remote/main'; import * as remoteMain from '@electron/remote/main';
import ipcHandlers from './ipc-handlers'; import ipcHandlers from './ipc-handlers';
import { shortcuts } from 'common/shortcuts';
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 +24,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 +32,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 +85,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 +121,9 @@ else {
mainWindow = await createMainWindow(); mainWindow = await createMainWindow();
createAppMenu(); createAppMenu();
if (isWindows)
mainWindow.show();
if (isDevelopment) if (isDevelopment)
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
@@ -114,6 +143,33 @@ else {
window.webContents.session.loadExtension(extensionPath, { allowFileAccess: true }).catch(console.error); window.webContents.session.loadExtension(extensionPath, { allowFileAccess: true }).catch(console.error);
} }
}); });
app.on('browser-window-focus', () => {
// Send registered shortcut events to window
for (const shortcut of shortcuts) {
if (shortcut.os.includes(process.platform)) {
for (const key of shortcut.keys) {
globalShortcut.register(key, () => {
mainWindow.webContents.send(shortcut.event);
if (isDevelopment) console.log('EVENT:', shortcut);
});
}
}
}
if (isDevelopment) { // Dev shortcuts
globalShortcut.register('Shift+CommandOrControl+F5', () => {
mainWindow.reload();
});
globalShortcut.register('Shift+CommandOrControl+F12', () => {
mainWindow.webContents.openDevTools();
});
}
});
app.on('browser-window-blur', () => {
globalShortcut.unregisterAll();
});
} }
function createAppMenu () { function createAppMenu () {

View File

@@ -2,7 +2,7 @@
<div id="wrapper" :class="[`theme-${applicationTheme}`, !disableBlur || 'no-blur']"> <div id="wrapper" :class="[`theme-${applicationTheme}`, !disableBlur || 'no-blur']">
<TheTitleBar /> <TheTitleBar />
<div id="window-content"> <div id="window-content">
<TheSettingBar /> <TheSettingBar @show-connections-modal="isAllConnectionsModal = true" />
<div id="main-content" class="container"> <div id="main-content" class="container">
<div class="columns col-gapless"> <div class="columns col-gapless">
<Workspace <Workspace
@@ -10,7 +10,7 @@
:key="connection.uid" :key="connection.uid"
:connection="connection" :connection="connection"
/> />
<div class="connection-panel-wrapper"> <div class="connection-panel-wrapper p-relative">
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" /> <WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div> </div>
</div> </div>
@@ -21,103 +21,104 @@
<BaseTextEditor class="d-none" value="" /> <BaseTextEditor class="d-none" value="" />
</div> </div>
</div> </div>
<ModalAllConnections v-if="isAllConnectionsModal" @close="isAllConnectionsModal = false" />
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent, onMounted, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { useI18n } from 'vue-i18n';
import { Menu, getCurrentWindow } from '@electron/remote'; import { Menu, getCurrentWindow } from '@electron/remote';
import { useApplicationStore } from '@/stores/application'; 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 { const { t } = useI18n();
name: 'App',
components: {
TheTitleBar: defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar')),
TheSettingBar,
TheFooter: defineAsyncComponent(() => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter')),
TheNotificationsBoard: defineAsyncComponent(() => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard')),
Workspace: defineAsyncComponent(() => import(/* webpackChunkName: "Workspace" */'@/components/Workspace')),
WorkspaceAddConnectionPanel: defineAsyncComponent(() => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel')),
ModalSettings: defineAsyncComponent(() => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')),
TheScratchpad: defineAsyncComponent(() => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad')),
BaseTextEditor: defineAsyncComponent(() => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor'))
},
setup () {
const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { const TheTitleBar = defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar.vue'));
isLoading, const TheFooter = defineAsyncComponent(() => import(/* webpackChunkName: "TheFooter" */'@/components/TheFooter.vue'));
isSettingModal, const TheNotificationsBoard = defineAsyncComponent(() => import(/* webpackChunkName: "TheNotificationsBoard" */'@/components/TheNotificationsBoard.vue'));
isScratchpad const Workspace = defineAsyncComponent(() => import(/* webpackChunkName: "Workspace" */'@/components/Workspace.vue'));
} = storeToRefs(applicationStore); const WorkspaceAddConnectionPanel = defineAsyncComponent(() => import(/* webpackChunkName: "WorkspaceAddConnectionPanel" */'@/components/WorkspaceAddConnectionPanel.vue'));
const { connections } = storeToRefs(connectionsStore); const ModalSettings = defineAsyncComponent(() => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings.vue'));
const { applicationTheme, disableBlur } = storeToRefs(settingsStore); const ModalAllConnections = defineAsyncComponent(() => import(/* webpackChunkName: "ModalAllConnections" */'@/components/ModalAllConnections.vue'));
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const TheScratchpad = defineAsyncComponent(() => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad.vue'));
const BaseTextEditor = defineAsyncComponent(() => import(/* webpackChunkName: "BaseTextEditor" */'@/components/BaseTextEditor.vue'));
const { checkVersionUpdate } = applicationStore; const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
return { const {
isLoading, isSettingModal,
isSettingModal, isScratchpad
isScratchpad, } = storeToRefs(applicationStore);
checkVersionUpdate, const { connections } = storeToRefs(connectionsStore);
connections, const { applicationTheme, disableBlur } = storeToRefs(settingsStore);
applicationTheme, const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
disableBlur,
selectedWorkspace
};
},
mounted () {
ipcRenderer.send('check-for-updates');
this.checkVersionUpdate();
const InputMenu = Menu.buildFromTemplate([ const { checkVersionUpdate } = applicationStore;
{ const { changeApplicationTheme } = settingsStore;
label: this.$t('word.cut'),
role: 'cut' const isAllConnectionsModal: Ref<boolean> = ref(false);
},
{ ipcRenderer.on('open-connections-modal', () => {
label: this.$t('word.copy'), isAllConnectionsModal.value = true;
role: 'copy' });
},
{ document.addEventListener('DOMContentLoaded', () => {
label: this.$t('word.paste'), setTimeout(() => {
role: 'paste' changeApplicationTheme(applicationTheme.value);// Forces persistentStore to save on file and mail process
}, }, 1000);
{ });
type: 'separator'
}, onMounted(() => {
{ ipcRenderer.send('check-for-updates');
label: this.$t('message.selectAll'), checkVersionUpdate();
role: 'selectall'
const InputMenu = Menu.buildFromTemplate([
{
label: t('word.cut'),
role: 'cut'
},
{
label: t('word.copy'),
role: 'copy'
},
{
label: t('word.paste'),
role: 'paste'
},
{
type: 'separator'
},
{
label: t('message.selectAll'),
role: 'selectAll'
}
]);
document.body.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let node: any = e.target;
while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup({ window: getCurrentWindow() });
break;
} }
]); node = node.parentNode;
}
document.body.addEventListener('contextmenu', (e) => { });
e.preventDefault(); });
e.stopPropagation();
let node = e.target;
while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup(getCurrentWindow());
break;
}
node = node.parentNode;
}
});
}
};
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -147,14 +148,14 @@ 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: 10vh;
display: flex; display: flex;
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,111 @@
<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, watch } 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(null);
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;
};
watch(() => props.modelValue, () => {
currentValue.value = props.modelValue;
});
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

@@ -0,0 +1,358 @@
<template>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div ref="trapRef" class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-apps mr-1" />
<span class="cut-text">{{ $t('message.allConnections') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body py-0">
<div class="columns">
<div class="connections-search column col-12 columns col-gapless">
<div class="column col-12 mt-2">
<div ref="searchForm" class="form-group has-icon-right p-2 m-0">
<input
v-model="searchTerm"
class="form-input"
type="text"
:placeholder="t('message.searchForConnections')"
@keypress.esc="searchTerm = ''"
>
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" />
<i
v-else
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-4"
@click="searchTerm = ''"
/>
</div>
</div>
</div>
<TransitionGroup name="fade" :duration="{ enter: 200, leave: 200 }">
<div
v-for="connection in filteredConnections"
:key="connection.uid"
class="connection-block column col-md-6 col-lg-4 col-3 p-3"
tabindex="0"
@click.stop="selectConnection(connection.uid)"
@keypress.stop.enter="selectConnection(connection.uid)"
@mouseover="connectionHover = connection.uid"
@mouseleave="connectionHover = null"
>
<div class="panel">
<div class="panel-header p-2 text-center p-relative">
<figure class="avatar avatar-lg pt-1 mb-1">
<i class="settingbar-element-icon dbi" :class="[`dbi-${connection.client}`]" />
</figure>
<div class="panel-title h6 text-ellipsis">
{{ getConnectionName(connection.uid) }}
</div>
<div class="panel-subtitle">
{{ clients.get(connection.client) || connection.client }}
</div>
<div class="all-connections-buttons p-absolute d-flex" style="top: 0; right: 0;">
<i
v-if="connection.isPinned"
class="all-connections-pinned mdi mdi-18px"
:class="connectionHover === connection.uid ? 'mdi-pin-off' : 'mdi-pin'"
:title="t('word.unpin')"
@click.stop="unpinConnection(connection.uid)"
/>
<i
v-else
class="all-connections-pin mdi mdi-18px mdi-pin mdi-rotate-45"
:title="t('word.pin')"
@click.stop="pinConnection(connection.uid)"
/>
<i
class="all-connections-delete mdi mdi-delete mdi-18px ml-2"
:title="t('word.delete')"
@click.stop="askToDelete(connection)"
/>
</div>
</div>
<div class="panel-body text-center">
<div v-if="connection.databasePath">
<div class="text-ellipsis" :title="connection.databasePath">
<i class="mdi mdi-database d-inline" /> <span class="text-bold">{{
connection.databasePath
}}</span>
</div>
</div>
<div v-else>
<div class="text-ellipsis" :title="`${connection.host}:${connection.port}`">
<i class="mdi mdi-server d-inline" /> <span class="text-bold">{{ connection.host
}}:{{ connection.port }}</span>
</div>
</div>
<div v-if="connection.user">
<div class="text-ellipsis">
<i class="mdi mdi-account d-inline" /> <span class="text-bold">{{ connection.user
}}</span>
</div>
</div>
<div v-if="connection.schema">
<div class="text-ellipsis">
<i class="mdi mdi-database d-inline" /> <span class="text-bold">{{ connection.schema
}}</span>
</div>
</div>
<div v-if="connection.database">
<div class="text-ellipsis">
<i class="mdi mdi-database d-inline" /> <span class="text-bold">{{
connection.database
}}</span>
</div>
</div>
</div>
<div class="panel-footer text-center py-0">
<div v-if="connection.ssl" class="chip bg-success mt-2">
<i class="mdi mdi-lock mdi-18px mr-1" />
SSL
</div>
<div v-if="connection.ssh" class="chip bg-success mt-2">
<i class="mdi mdi-console-network mdi-18px mr-1" />
SSH
</div>
</div>
</div>
</div>
<input
key="trick"
readonly
class="p-absolute"
style="width: 1px; height: 1px; opacity: 0"
type="text"
>
<!-- workaround for useFocusTrap $lastFocusable -->
</TransitionGroup>
</div>
</div>
</div>
</div>
<ConfirmModal
v-if="isConfirmModal"
@confirm="confirmDeleteConnection"
@hide="isConfirmModal = false"
>
<template #header>
<div class="d-flex">
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ t('message.deleteConnection') }}
</div>
</template>
<template #body>
<div class="mb-2">
{{ t('message.deleteCorfirm') }} <b>{{ selectedConnectionName }}</b>?
</div>
</template>
</ConfirmModal>
</Teleport>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useFocusTrap } from '@/composables/useFocusTrap';
import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { ConnectionParams } from 'common/interfaces/antares';
const { t } = useI18n();
const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { connections,
pinnedConnections,
lastConnections
} = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getConnectionName,
pinConnection,
unpinConnection,
deleteConnection
} = connectionsStore;
const { selectWorkspace } = workspacesStore;
const { trapRef } = useFocusTrap();
const emit = defineEmits(['close']);
const clients = new Map([
['mysql', 'MySQL'],
['maria', 'MariaDB'],
['pg', 'PostgreSQL'],
['sqlite', 'SQLite']
]);
const searchTerm = ref('');
const isConfirmModal = ref(false);
const connectionHover: Ref<string> = ref(null);
const selectedConnection: Ref<ConnectionParams> = ref(null);
const sortedConnections = computed(() => {
return connections.value
.map(c => {
const connTime = lastConnections.value.find((lc) => lc.uid === c.uid)?.time || 0;
return {
...c,
time: connTime,
isPinned: pinnedConnections.value.has(c.uid)
};
})
.sort((a, b) => {
if (a.isPinned < b.isPinned) return 1;
if (a.isPinned > b.isPinned) return -1;
if (a.time < b.time) return 1;
if (a.time > b.time) return -1;
return 0;
});
});
const filteredConnections = computed(() => {
return sortedConnections.value.filter(connection => {
return connection.name?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.host?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.database?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.databasePath?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.schema?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.user?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
String(connection.port)?.includes(searchTerm.value);
});
});
const selectedConnectionName = computed(() => getConnectionName(selectedConnection.value?.uid));
const closeModal = () => emit('close');
const selectConnection = (uid: string) => {
selectWorkspace(uid);
closeModal();
};
const askToDelete = (connection: ConnectionParams) => {
selectedConnection.value = connection;
isConfirmModal.value = true;
};
const confirmDeleteConnection = () => {
if (selectedWorkspace.value === selectedConnection.value.uid)
selectWorkspace(null);
deleteConnection(selectedConnection.value);
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape') {
if ((e.target as HTMLInputElement).tagName === 'INPUT' && searchTerm.value.length > 0)
searchTerm.value = '';
else
closeModal();
}
};
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.column-resizable {
&:hover,
&:active {
resize: horizontal;
overflow: hidden;
}
}
.table-column-title {
display: flex;
align-items: center;
}
.sort-icon {
font-size: 0.7rem;
line-height: 1;
margin-left: 0.2rem;
}
.modal {
align-items: flex-start;
.modal-container {
max-width: 75vw;
margin-top: 10vh;
.modal-body {
height: 80vh;
}
}
}
.connections-search {
display: flex;
justify-content: space-around;
}
.connection-block {
cursor: pointer;
transition: all .2s;
border-radius: $border-radius;
outline: none;
&:focus {
box-shadow: 0 0 3px .1rem rgba($primary-color, 80%);
}
&:hover {
.all-connections-buttons {
.all-connections-delete,
.all-connections-pinned,
.all-connections-pin {
opacity: .5;
}
}
}
.all-connections-buttons {
.all-connections-pinned {
opacity: .3;
transition: opacity .2s;
&:hover {
opacity: 1;
}
}
.all-connections-delete,
.all-connections-pin {
opacity: 0;
transition: opacity .2s;
&:hover {
opacity: 1;
}
}
}
}
</style>

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,213 @@
</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 { ClientCode, SchemaInfos } from 'common/interfaces/antares';
import { ExportOptions, ExportState } 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<{
includeDropStatement: true table: string;
})); includeStructure: boolean;
includeContent: boolean;
includeDropStatement: boolean;
}[]> = ref([]);
const options: Ref<Partial<ExportOptions>> = ref({
schema: props.selectedSchema,
includes: {} as {[key: string]: boolean},
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 = { } as ExportOptions & { uid: string; type: ClientCode };
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 +478,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 +502,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,76 +97,197 @@
</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: { schema: String,
FakerSelect table: String,
}, fields: Array as Prop<TableField[]>,
props: { // eslint-disable-next-line @typescript-eslint/no-explicit-any
tabUid: [String, Number], rowToDuplicate: Object as Prop<any>,
fields: Array, keyUsage: Array as Prop<TableForeign[]>
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, const { trapRef } = useFocusTrap({ disableAutofocus: true });
getWorkspace,
getWorkspaceTab // eslint-disable-next-line @typescript-eslint/no-explicit-any
}; const localRow: Ref<{[key: string]: any}> = ref({});
}, const fieldsToExclude = ref([]);
data () { const nInserts = ref(1);
return { const isInserting = ref(false);
localRow: {}, const fakerLocale = ref('en');
fieldsToExclude: [],
nInserts: 1, const foreignKeys = computed(() => props.keyUsage.map(key => key.field));
isInserting: false, const hasFakes = computed(() => Object.keys(localRow.value).some(field => 'group' in localRow.value[field] && localRow.value[field].group !== 'manual'));
fakerLocale: 'en'
}; const locales = [
}, { value: 'ar', label: 'Arabic' },
computed: { { value: 'az', label: 'Azerbaijani' },
workspace () { { value: 'zh_CN', label: 'Chinese' },
return this.getWorkspace(this.selectedWorkspace); { value: 'zh_TW', label: 'Chinese (Taiwan)' },
}, { value: 'cz', label: 'Czech' },
foreignKeys () { { value: 'nl', label: 'Dutch' },
return this.keyUsage.map(key => key.field); { value: 'nl_BE', label: 'Dutch (Belgium)' },
}, { value: 'en', label: 'English' },
hasFakes () { { value: 'en_AU_ocker', label: 'English (Australia Ocker)' },
return Object.keys(this.localRow).some(field => 'group' in this.localRow[field] && this.localRow[field].group !== 'manual'); { value: 'en_AU', label: 'English (Australia)' },
{ value: 'en_BORK', label: 'English (Bork)' },
{ value: 'en_CA', label: 'English (Canada)' },
{ 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' }
];
watch(nInserts, (val) => {
if (!val || val < 1)
nInserts.value = 1;
else if (val > 1000)
nInserts.value = 1000;
});
const typeClass = (type: string) => {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
};
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: props.schema,
table: props.table,
row: rowToInsert,
repeat: nInserts.value,
fields: fieldTypes,
locale: fakerLocale.value
});
if (status === 'success') {
closeModal();
emit('reload');
} }
}, else
watch: { addNotification({ status: 'error', message: response });
nInserts (val) { }
if (!val || val < 1) catch (err) {
this.nInserts = 1; addNotification({ status: 'error', message: err.stack });
else if (val > 1000) }
this.nInserts = 1000;
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
const rowObj = {};
for (const field of this.fields) { 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} = {};
if (!props.rowToDuplicate) {
// Set default values
for (const field of props.fields) {
let fieldDefault; let fieldDefault;
if (field.default === 'NULL') fieldDefault = null; if (field.default === 'NULL') fieldDefault = null;
@@ -271,7 +304,7 @@ export default {
else if ([...TIME, ...DATE].includes(field.type)) else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default; fieldDefault = field.default;
else if (BIT.includes(field.type)) else if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', ''); fieldDefault = field.default?.replaceAll('\'', '').replaceAll('b', '');
else if (DATETIME.includes(field.type)) { else if (DATETIME.includes(field.type)) {
if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) { if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) {
let datePrecision = ''; let datePrecision = '';
@@ -291,95 +324,26 @@ export default {
rowObj[field.name] = { value: fieldDefault }; rowObj[field.name] = { value: fieldDefault };
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
this.fieldsToExclude = [...this.fieldsToExclude, field.name]; fieldsToExclude.value = [...fieldsToExclude.value, field.name];
}
this.localRow = { ...rowObj };
},
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.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})`;
} }
} }
}; else {
// Set values to duplicate
for (const field of props.fields) {
if (typeof props.rowToDuplicate[field.name] !== 'object')
rowObj[field.name] = { value: props.rowToDuplicate[field.name] };
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 { exportRows } from '../libs/exportRows';
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;
exportRows({
type: format,
content: sortedResults.value,
table: '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">
@@ -137,10 +126,23 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ t('message.disableScratchpad') }}
</label>
</div>
<div class="col-3 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleDisableScratchpad">
<input type="checkbox" :checked="disableScratchpad">
<i class="form-icon" />
</label>
</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.notificationsTimeout') }} {{ t('message.notificationsTimeout') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@@ -152,19 +154,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 +179,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 +198,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 +218,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 +230,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 +251,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 +291,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 +314,126 @@
</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,
disableScratchpad,
applicationTheme,
editorTheme,
editorFontSize
} = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
changeLocale,
changePageSize,
changeRestoreTabs,
changeDisableBlur,
changeDisableScratchpad,
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 +448,85 @@ 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 toggleDisableScratchpad = () => {
changeDisableScratchpad(!disableScratchpad.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,369 @@ 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 lastTableFields = ref([]);
customCompleter: [], const customCompleter = ref([]);
id: uidGen(), const id = ref(uidGen());
lastSchema: null const lastSchema: Ref<string> = ref(null);
}; const fields: Ref<{name: string; type: string}[]> = ref([]);
},
computed: { const tables = computed(() => {
tables () { 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.tables);
acc.push(...curr.tables); return acc;
return acc; }, []).map(table => {
}, []).map(table => { return {
return { name: table.name as string,
name: table.name, type: table.type as string
type: table.type, };
fields: [] })
}; : [];
}) });
: [];
}, const tablesInQuery = computed(() => {
triggers () { if (!props.modelValue) return [];
return this.workspace const words = props.modelValue
? this.workspace.structure.filter(schema => schema.name === this.schema) .replaceAll(/[.'"`]/g, ' ')
.reduce((acc, curr) => { .split(' ')
acc.push(...curr.triggers); .filter(Boolean);
return acc;
}, []).map(trigger => { const includedTables = tables.value.reduce((acc, curr) => {
return { acc.push(curr.name);
name: trigger.name, return acc;
type: 'trigger' }, [] as string[]).filter((t) => words.includes(t));
};
}) return includedTables;
: []; });
},
procedures () { const triggers = 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.triggers);
return acc; return acc;
}, []).map(procedure => { }, []).map(trigger => {
return { return {
name: `${procedure.name}()`, name: trigger.name as string,
type: 'routine' type: 'trigger'
}; };
}) })
: []; : [];
}, });
functions () {
return this.workspace const procedures = 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.procedures);
}, []).map(func => { return acc;
return { }, []).map(procedure => {
name: `${func.name}()`, return {
type: 'function' name: `${procedure.name}()`,
}; type: 'routine'
}) };
: []; })
}, : [];
schedulers () { });
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema) const functions = 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.functions);
return { return acc;
name: scheduler.name, }, []).map(func => {
type: 'scheduler' return {
}; name: `${func.name}()`,
}) type: 'function'
: []; };
}, })
mode () { : [];
switch (this.workspace.client) { });
case 'mysql':
case 'maria': const schedulers = computed(() => {
return 'mysql'; return props.workspace
case 'mssql': ? props.workspace.structure.filter(schema => schema.name === props.schema)
return 'sqlserver'; .reduce((acc, curr) => {
case 'pg': acc.push(...curr.schedulers);
return 'pgsql'; return acc;
default: }, []).map(scheduler => {
return 'sql'; return {
} name: scheduler.name as string,
}, type: 'scheduler'
lastWord () { };
const charsBefore = this.modelValue.slice(0, this.cursorPosition); })
const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean); : [];
return words.pop(); });
},
isLastWordATable () { const mode = computed(() => {
return /\w+\.\w*/gm.test(this.lastWord); switch (props.workspace.client) {
}, case 'mysql':
fieldsCompleter () { case 'maria':
return { return 'mysql';
getCompletions: (editor, session, pos, prefix, callback) => { // case 'mssql':
const completions = []; // return 'sqlserver';
this.fields.forEach(field => { case 'pg':
completions.push({ return 'pgsql';
value: field, default:
meta: 'column', return 'sql';
score: 1000 }
}); });
});
callback(null, completions); const lastWord = computed(() => {
} const charsBefore = props.modelValue.slice(0, cursorPosition.value);
}; const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean);
return words.pop();
});
const isLastWordATable = computed(() => /\w+\.\w*/gm.test(lastWord.value));
const tableFieldsCompleter = computed(() => {
return {
getCompletions: (editor: never, session: never, pos: never, prefix: never, callback: (err: null, response: ace.Ace.Completion[]) => void) => {
const completions: ace.Ace.Completion[] = [];
lastTableFields.value.forEach(field => {
completions.push({
value: field,
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,
...fields.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(() => tablesInQuery.value.length, () => {
readOnly: this.readOnly const localFields: {name: string; type: string}[] = [];
tablesInQuery.value.forEach(async table => {
const params = {
uid: props.workspace.uid,
schema: props.schema,
table: table
};
const { response } = await Tables.getTableColumns(params);
response.forEach((field: { name: string }) => {
localFields.push({
name: field.name,
type: 'column'
});
}); });
});
this.editor.setOptions({ fields.value = localFields;
enableBasicAutocompletion: true, setCustomCompleter();
wrap: this.lineWrap, });
enableSnippets: true,
enableLiveAutocompletion: this.autoComplete watch(editorTheme, () => {
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]
}); });
}
});
if (!this.baseCompleter.length) watch(autoComplete, () => {
this.setBaseCompleters(this.editor.completers.map(el => Object.assign({}, el))); if (editor.value) {
editor.value.setOptions({
enableLiveAutocompletion: autoComplete.value
});
}
});
this.setCustomCompleter(); watch(lineWrap, () => {
if (editor.value) {
editor.value.setOptions({
wrap: lineWrap.value
});
}
});
this.editor.commands.on('afterExec', e => { watch(() => props.isSelected, () => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) { if (props.isSelected) {
if (this.isLastWordATable || e.args === '.') { lastSchema.value = props.schema;
if (e.args !== ' ') { editor.value.resize();
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop().trim()); }
});
if (table) { watch(() => props.height, () => {
const params = { setTimeout(() => {
uid: this.workspace.uid, editor.value.resize();
schema: this.schema, }, 20);
table: table.name });
};
Tables.getTableColumns(params).then(res => { watch(lastSchema, () => {
if (res.response.length) if (editor.value) {
this.fields = res.response.map(field => field.name); editor.value.completers = baseCompleter.value.map(el => Object.assign({}, el));
this.editor.completers = [this.fieldsCompleter]; setCustomCompleter();
this.editor.execCommand('startAutocomplete'); }
}).catch(console.log); });
}
else lastSchema.value = toRef(props, 'schema').value;
this.editor.completers = this.customCompleter;
onMounted(() => {
editor.value = ace.edit(`editor-${id.value}`, {
mode: `ace/mode/${mode.value}`,
theme: `ace/theme/${editorTheme.value}`,
value: props.modelValue,
fontSize: 14,
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)
lastTableFields.value = res.response.map((field: { name: string }) => field.name);
editor.value.completers = [tableFieldsCompleter.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,27 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="$emit('close-context')" @close-context="$emit('close-context')"
> >
<div
v-if="isPinned"
class="context-element"
@click="unpin"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-pin-off text-light pr-1" /> {{ $t('word.unpin') }}</span>
</div>
<div
v-else
class="context-element"
@click="pin"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-pin mdi-rotate-45 text-light pr-1" /> {{ $t('word.pin') }}</span>
</div>
<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 +50,94 @@
</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 connectionsStore = useConnectionsStore();
name: 'SettingBarContext',
components: {
BaseContextMenu,
ConfirmModal
},
props: {
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 {
getConnectionName,
addConnection,
deleteConnection,
pinConnection,
unpinConnection
} = connectionsStore;
return { const { pinnedConnections } = storeToRefs(connectionsStore);
getConnectionName,
addConnection,
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 workspacesStore = useWorkspacesStore();
this.closeContext(); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
},
showConfirmModal () { const {
this.isConfirmModal = true; selectWorkspace,
}, removeConnected: disconnectWorkspace,
hideConfirmModal () { getWorkspace
this.isConfirmModal = false; } = workspacesStore;
this.closeContext();
}, const props = defineProps({
closeContext () { contextEvent: MouseEvent,
this.$emit('close-context'); contextConnection: Object as Prop<ConnectionParams>
} });
}
const emit = defineEmits(['close-context']);
const isConfirmModal = ref(false);
const connectionName = computed(() => getConnectionName(props.contextConnection.uid));
const isConnected = computed(() => getWorkspace(props.contextConnection.uid).connectionStatus === 'connected');
const isPinned = computed(() => pinnedConnections.value.has(props.contextConnection.uid));
const confirmDeleteConnection = () => {
if (selectedWorkspace.value === props.contextConnection.uid)
selectWorkspace(null);
deleteConnection(props.contextConnection);
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 pin = () => {
pinConnection(props.contextConnection.uid);
closeContext();
};
const unpin = () => {
unpinConnection(props.contextConnection.uid);
closeContext();
};
const disconnect = () => {
disconnectWorkspace(props.contextConnection.uid);
closeContext();
};
const closeContext = () => {
emit('close-context');
}; };
</script> </script>

View File

@@ -11,14 +11,30 @@
<div class="footer-right-elements"> <div class="footer-right-elements">
<ul class="footer-elements"> <ul class="footer-elements">
<li
v-if="workspace?.connectionStatus === 'connected' "
class="footer-element footer-link"
@click="toggleConsole()"
>
<i class="mdi mdi-18px mdi-console-line mr-1" />
<small>{{ t('word.console') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://www.paypal.com/paypalme/fabiodistasio')"> <li class="footer-element footer-link" @click="openOutside('https://www.paypal.com/paypalme/fabiodistasio')">
<i class="mdi mdi-18px mdi-coffee mr-1" /> <i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small> <small>{{ t('word.donate') }}</small>
</li> </li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/antares-sql/antares/issues')"> <li
class="footer-element footer-link"
:title="t('message.reportABug')"
@click="openOutside('https://github.com/antares-sql/antares/issues')"
>
<i class="mdi mdi-18px mdi-bug" /> <i class="mdi mdi-18px mdi-bug" />
</li> </li>
<li class="footer-element footer-link" @click="showSettingModal('about')"> <li
class="footer-element footer-link"
:title="t('word.about')"
@click="showSettingModal('about')"
>
<i class="mdi mdi-18px mdi-information-outline" /> <i class="mdi mdi-18px mdi-information-outline" />
</li> </li>
</ul> </ul>
@@ -26,46 +42,45 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { shell } from 'electron';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
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'); import { useConsoleStore } from '@/stores/console';
export default { const { t } = useI18n();
name: 'TheFooter',
setup () {
const applicationStore = useApplicationStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: workspace } = storeToRefs(workspacesStore); interface DatabaseInfos {// TODO: temp
name: string;
number: string;
arch: string;
os: string;
}
const { appVersion, showSettingModal } = applicationStore; const applicationStore = useApplicationStore();
const { getWorkspace } = workspacesStore; const workspacesStore = useWorkspacesStore();
return { const { getSelected: workspaceUid } = storeToRefs(workspacesStore);
appVersion, const { toggleConsole } = useConsoleStore();
showSettingModal,
workspace, const { showSettingModal } = applicationStore;
getWorkspace const { getWorkspace } = workspacesStore;
};
}, const workspace = computed(() => getWorkspace(workspaceUid.value));
computed: { const version: ComputedRef<DatabaseInfos> = computed(() => {
version () { return getWorkspace(workspaceUid.value) ? workspace.value.version : null;
return this.getWorkspace(this.workspace) ? this.getWorkspace(this.workspace).version : null; });
},
versionString () { const versionString = computed(() => {
if (this.version) if (version.value)
return `${this.version.name} ${this.version.number} (${this.version.arch} ${this.version.os})`; return `${version.value.name} ${version.value.number} (${version.value.arch} ${version.value.os})`;
return ''; return '';
} });
},
methods: { const openOutside = (link: string) => shell.openExternal(link);
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

@@ -1,6 +1,6 @@
<template> <template>
<div id="settingbar"> <div id="settingbar">
<div class="settingbar-top-elements"> <div ref="sidebarConnections" class="settingbar-top-elements">
<SettingBarContext <SettingBarContext
v-if="isContext" v-if="isContext"
:context-event="contextEvent" :context-event="contextEvent"
@@ -9,7 +9,7 @@
/> />
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<Draggable <Draggable
v-model="connections" v-model="pinnedConnectionsArr"
:item-key="'uid'" :item-key="'uid'"
@start="isDragging = true" @start="isDragging = true"
@end="dragStop" @end="dragStop"
@@ -23,11 +23,40 @@
@contextmenu.prevent="contextMenu($event, element)" @contextmenu.prevent="contextMenu($event, element)"
@mouseover.self="tooltipPosition" @mouseover.self="tooltipPosition"
> >
<i class="settingbar-element-icon dbi" :class="`dbi-${element.client} ${getStatusBadge(element.uid)}`" /> <i class="settingbar-element-icon dbi" :class="[`dbi-${element.client}`, getStatusBadge(element.uid), (pinnedConnections.has(element.uid) ? 'settingbar-element-pin' : false)]" />
<span v-if="!isDragging" class="ex-tooltip-content">{{ getConnectionName(element.uid) }}</span> <span v-if="!isDragging && !isScrolling" class="ex-tooltip-content">{{ getConnectionName(element.uid) }}</span>
</li> </li>
</template> </template>
</Draggable> </Draggable>
<div v-if="pinnedConnectionsArr.length" class="divider" />
<li
v-for="connection in unpinnedConnectionsArr"
:key="connection.uid"
class="settingbar-element btn btn-link ex-tooltip"
:class="{'selected': connection.uid === selectedWorkspace}"
@click.stop="selectWorkspace(connection.uid)"
@contextmenu.prevent="contextMenu($event, connection)"
@mouseover.self="tooltipPosition"
>
<i class="settingbar-element-icon dbi" :class="[`dbi-${connection.client}`, getStatusBadge(connection.uid)]" />
<span v-if="!isDragging && !isScrolling" class="ex-tooltip-content">{{ getConnectionName(connection.uid) }}</span>
</li>
</ul>
</div>
<div class="settingbar-middle-elements">
<ul class="settingbar-elements">
<li
v-if="isScrollable"
class="settingbar-element btn btn-link ex-tooltip"
@click="emit('show-connections-modal')"
@mouseover.self="tooltipPosition"
>
<i class="settingbar-element-icon mdi mdi-24px mdi-dots-horizontal text-light" />
<span class="ex-tooltip-content">{{ $t('message.allConnections') }} (Shift+CTRL+Space)</span>
</li>
<li <li
class="settingbar-element btn btn-link ex-tooltip" class="settingbar-element btn btn-link ex-tooltip"
:class="{'selected': 'NEW' === selectedWorkspace}" :class="{'selected': 'NEW' === selectedWorkspace}"
@@ -42,7 +71,11 @@
<div class="settingbar-bottom-elements"> <div class="settingbar-bottom-elements">
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<li class="settingbar-element btn btn-link ex-tooltip" @click="showScratchpad"> <li
v-if="!disableScratchpad"
class="settingbar-element btn btn-link ex-tooltip"
@click="showScratchpad"
>
<i class="settingbar-element-icon mdi mdi-24px mdi-notebook-edit-outline text-light" /> <i class="settingbar-element-icon mdi mdi-24px mdi-notebook-edit-outline text-light" />
<span class="ex-tooltip-content">{{ $t('word.scratchpad') }}</span> <span class="ex-tooltip-content">{{ $t('word.scratchpad') }}</span>
</li> </li>
@@ -55,108 +88,152 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { ref, Ref, computed, watch } 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 { useSettingsStore } from '@/stores/settings';
import SettingBarContext from '@/components/SettingBarContext'; import * as Draggable from 'vuedraggable';
import SettingBarContext from '@/components/SettingBarContext.vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { useElementBounding, useScroll } from '@vueuse/core';
export default { const applicationStore = useApplicationStore();
name: 'TheSettingBar', const connectionsStore = useConnectionsStore();
components: { const workspacesStore = useWorkspacesStore();
Draggable, const settingsStore = useSettingsStore();
SettingBarContext
},
setup () {
const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { updateStatus } = storeToRefs(applicationStore); const { updateStatus } = storeToRefs(applicationStore);
const { connections: getConnections } = storeToRefs(connectionsStore); const { connections: storedConnections, pinnedConnections, lastConnections } = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { disableScratchpad } = storeToRefs(settingsStore);
const { showSettingModal, showScratchpad } = applicationStore; const { showSettingModal, showScratchpad } = applicationStore;
const { getConnectionName, updateConnections } = connectionsStore; const { getConnectionName, updatePinnedConnections } = connectionsStore;
const { getWorkspace, selectWorkspace } = workspacesStore; const { getWorkspace, selectWorkspace } = workspacesStore;
return { const emit = defineEmits(['show-connections-modal']);
applicationStore,
updateStatus,
showSettingModal,
showScratchpad,
getConnections,
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 isLinux = process.platform === 'linux';
case 'connected':
return 'badge badge-connected';
case 'connecting':
return 'badge badge-connecting';
case 'failed':
return 'badge badge-failed';
default:
return '';
}
}
},
dragStop (e) {
this.isDragging = false;
setTimeout(() => { const sidebarConnections: Ref<HTMLDivElement> = ref(null);
this.tooltipPosition(e.originalEvent.target.parentNode); const isContext: Ref<boolean> = ref(false);
}, 200); const isDragging: Ref<boolean> = ref(false);
const isScrollable: Ref<boolean> = ref(false);
const isScrolling = ref(useScroll(sidebarConnections)?.isScrolling);
const contextEvent: Ref<MouseEvent> = ref(null);
const contextConnection: Ref<ConnectionParams> = ref(null);
const sidebarConnectionsHeight = ref(useElementBounding(sidebarConnections)?.height);
const pinnedConnectionsArr = computed({
get: () => [...pinnedConnections.value].map(c => storedConnections.value.find(sc => sc.uid === c)).filter(Boolean),
set: (value: ConnectionParams[]) => {
const pinnedUid = value.reduce((acc, curr) => {
acc.push(curr.uid);
return acc;
}, []);
updatePinnedConnections(pinnedUid);
}
});
const unpinnedConnectionsArr = computed(() => {
return storedConnections.value
.filter(c => !pinnedConnections.value.has(c.uid))
.map(c => {
const connTime = lastConnections.value.find((lc) => lc.uid === c.uid)?.time || 0;
return { ...c, time: connTime };
})
.sort((a, b) => {
if (a.time < b.time) return 1;
else if (a.time > b.time) return -1;
return 0;
});
});
const hasUpdates = computed(() => ['available', 'downloading', 'downloaded', 'link'].includes(updateStatus.value));
const contextMenu = (event: MouseEvent, connection: ConnectionParams) => {
contextEvent.value = event;
contextConnection.value = connection;
isContext.value = true;
};
const tooltipPosition = (e: Event) => {
const el = (e.target ? e.target : e) as unknown as HTMLElement;
const tooltip = el.querySelector<HTMLElement>('.ex-tooltip-content');
if (tooltip) {
const fromTop = isLinux
? window.scrollY + el.getBoundingClientRect().top + (el.offsetHeight / 4)
: window.scrollY + el.getBoundingClientRect().top - (el.offsetHeight / 4);
tooltip.style.top = `${fromTop}px`;
}
};
const getStatusBadge = (uid: string) => {
if (getWorkspace(uid)) {
const status = getWorkspace(uid).connectionStatus;
switch (status) {
case 'connected':
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) => {
isDragging.value = false;
setTimeout(() => {
tooltipPosition(e.originalEvent.target.parentNode);
}, 200);
};
watch(sidebarConnectionsHeight, (value) => {
isScrollable.value = value < sidebarConnections.value.scrollHeight;
});
watch(unpinnedConnectionsArr, (newVal, oldVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
setTimeout(() => {
const element = document.querySelector<HTMLElement>('.settingbar-element.selected');
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);
}
});
watch(selectedWorkspace, (newVal, oldVal) => {
if (newVal !== oldVal) {
setTimeout(() => {
const element = document.querySelector<HTMLElement>('.settingbar-element.selected');
if (element) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
}, 150);
}
});
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -165,7 +242,7 @@ export default {
height: calc(100vh - #{$excluding-size}); height: calc(100vh - #{$excluding-size});
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; // justify-content: space-between;
align-items: center; align-items: center;
padding: 0; padding: 0;
z-index: 9; z-index: 9;
@@ -173,7 +250,7 @@ export default {
.settingbar-top-elements { .settingbar-top-elements {
overflow-x: hidden; overflow-x: hidden;
overflow-y: overlay; overflow-y: overlay;
max-height: calc((100vh - 3.5rem) - #{$excluding-size}); // max-height: calc((100vh - 3.5rem) - #{$excluding-size});
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 3px; width: 3px;
@@ -181,8 +258,8 @@ export default {
} }
.settingbar-bottom-elements { .settingbar-bottom-elements {
padding-top: 0.5rem;
z-index: 1; z-index: 1;
margin-top: auto;
} }
.settingbar-elements { .settingbar-elements {
@@ -238,6 +315,21 @@ export default {
bottom: initial; bottom: initial;
} }
} }
.settingbar-element-pin{
margin: 0 auto;
&::before {
font: normal normal normal 14px/1 "Material Design Icons";
content: "\F0403";
color: $body-font-color-dark;
transform: rotate(45deg);
opacity: .25;
bottom: -8px;
left: -4px;
position: absolute;
}
}
} }
} }
} }

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

@@ -279,6 +279,12 @@
<span>{{ $t('message.processesList') }}</span> <span>{{ $t('message.processesList') }}</span>
</a> </a>
</li> </li>
<li class="menu-item">
<a class="c-hand p-vcentered" @click="toggleConsole">
<i class="mdi mdi-console-line mr-1 tool-icon" />
<span>{{ $t('word.console') }}</span>
</a>
</li>
<li <li
v-if="workspace.customizations.variables" v-if="workspace.customizations.variables"
class="menu-item" class="menu-item"
@@ -315,10 +321,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 +331,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 +341,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 +349,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 +357,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 +365,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 +373,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 +382,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 +390,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 +399,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 +407,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 +416,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 +424,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 +433,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 +441,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 +450,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"
@@ -466,8 +457,9 @@
:schema="tab.schema" :schema="tab.schema"
/> />
</template> </template>
<WorkspaceQueryConsole v-if="isConsoleOpen(workspace.uid)" :uid="workspace.uid" />
</div> </div>
<div v-else class="connection-panel-wrapper"> <div v-else class="connection-panel-wrapper p-relative">
<WorkspaceEditConnectionPanel :connection="connection" /> <WorkspaceEditConnectionPanel :connection="connection" />
</div> </div>
<ModalProcessesList <ModalProcessesList
@@ -484,246 +476,216 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { ipcRenderer } from 'electron';
import { computed, onMounted, 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 { useConsoleStore } from '@/stores/console';
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 WorkspaceQueryConsole from '@/components/WorkspaceQueryConsole.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, selectNextTab,
WorkspaceTabNewTrigger, selectPrevTab
WorkspaceTabPropsTrigger, } = workspacesStore;
WorkspaceTabNewTriggerFunction,
WorkspaceTabPropsTriggerFunction, const consoleStore = useConsoleStore();
WorkspaceTabNewRoutine,
WorkspaceTabNewFunction, const { isConsoleOpen } = storeToRefs(consoleStore);
WorkspaceTabPropsRoutine, const { toggleConsole } = consoleStore;
WorkspaceTabPropsFunction,
WorkspaceTabNewScheduler, const props = defineProps({
WorkspaceTabPropsScheduler, connection: Object as Prop<ConnectionParams>
ModalProcessesList, });
ModalDiscardChanges
const hasWheelEvent = ref(false);
const isProcessesModal = ref(false);
const unsavedTab = ref(null);
const tabWrap = ref(null);
const workspace = computed(() => getWorkspace(props.connection.uid));
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', schema: workspace.value.breadcrumbs.schema });
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 openAsPermanentTab = (tab: WorkspaceTab) => {
}, const permanentTabs = {
showProcessesModal () { table: 'data',
this.isProcessesModal = true; view: 'data',
}, trigger: 'trigger-props',
hideProcessesModal () { triggerFunction: 'trigger-function-props',
this.isProcessesModal = false; function: 'function-props',
}, routine: 'routine-props',
addWheelEvent () { procedure: 'routine-props',
if (!this.hasWheelEvent) { scheduler: 'scheduler-props'
this.$refs.tabWrap.$el.addEventListener('wheel', e => { } as {[key: string]: string};
if (e.deltaY > 0) this.$refs.tabWrap.$el.scrollLeft += 50;
else this.$refs.tabWrap.$el.scrollLeft -= 50; newTab({
}); uid: props.connection.uid,
this.hasWheelEvent = true; schema: tab.schema,
} elementName: tab.elementName,
}, type: permanentTabs[tab.elementType],
cutText (string) { elementType: tab.elementType
const limit = 20; });
const escapedString = string.replace(/\s{2,}/g, ' '); };
if (escapedString.length > limit)
return `${escapedString.substr(0, limit)}...`; const closeTab = (tab: WorkspaceTab, force = false) => {
return escapedString; 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 () => {
await addWorkspace(props.connection.uid);
const isInitiated = await Connection.checkConnection(props.connection.uid);
if (isInitiated)
connectWorkspace(props.connection);
})();
onMounted(() => {
ipcRenderer.on('open-new-tab', () => {
if (!isSelected.value) return;
addQueryTab();
});
ipcRenderer.on('close-tab', () => {
if (!isSelected.value) return;
const currentTab = getSelectedTab();
if (currentTab)
closeTab(currentTab);
});
ipcRenderer.on('next-tab', () => {
if (!isSelected.value) return;
selectNextTab({ uid: props.connection.uid });
});
ipcRenderer.on('prev-tab', () => {
if (!isSelected.value) return;
selectPrevTab({ uid: props.connection.uid });
});
for (let i = 1; i <= 9; i++) {
ipcRenderer.on(`select-tab-${i}`, () => {
if (!isSelected.value) return;
if (workspace.value.tabs[i-1])
selectTab({ uid: props.connection.uid, tab: workspace.value.tabs[i-1].uid });
});
}
});
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -732,8 +694,9 @@ export default {
margin: 0; margin: 0;
.workspace-tabs { .workspace-tabs {
overflow: hidden; overflow-y: hidden;
height: calc(100vh - #{$excluding-size}); height: calc(100vh - #{$excluding-size});
position: relative;
.tab-block { .tab-block {
margin-top: 0; margin-top: 0;

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,193 +389,179 @@
</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 = [
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>
.connection-panel { .connection-panel {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-bottom: 1rem; margin-bottom: .5rem;
margin-top: 1.5rem;
.panel { .panel {
min-width: 450px; min-width: 450px;

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
@@ -253,6 +253,14 @@
> >
</div> </div>
</div> </div>
<div class="form-group columns">
<div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="localConnection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('message.untrustedConnection') }}
</label>
</div>
</div>
</fieldset> </fieldset>
</form> </form>
</div> </div>
@@ -263,7 +271,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 +284,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 +296,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 +308,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 +320,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 +334,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 +347,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 +370,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 +379,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 +389,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,159 +401,158 @@
</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 = [
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>
.connection-panel { .connection-panel {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-bottom: 1rem; margin-bottom: .5rem;
margin-top: 1.5rem;
.panel { .panel {
min-width: 450px; min-width: 450px;

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