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

Compare commits

...

235 Commits

Author SHA1 Message Date
dependabot[bot]
67e849bc66 chore(deps): bump webpack from 5.91.0 to 5.98.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.91.0 to 5.98.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.91.0...v5.98.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-01 19:25:34 +00:00
eb706c3e51 fix: issue with some SSH connections, definitely 2025-02-14 20:30:47 +01:00
971df3a989 chore(release): 0.7.33 2025-02-14 18:03:57 +01:00
3129bf4baa fix: issue with some SSH connections, fixes #947 2025-02-14 17:58:49 +01:00
c6d67cef01 chore(release): 0.7.32 2025-02-14 09:12:05 +01:00
1d7053ce03 fix: black background with light theme, fixes #945 2025-02-14 09:08:45 +01:00
41e797f9e2 fix(PostgreSQL): error with materialized view tabs 2025-02-13 18:01:19 +01:00
704f70819b fix: improve error handling in SSH connection 2025-02-12 18:10:27 +01:00
49a3589536 fix: enhance SVG support in connection customization, fixes #939 2025-02-12 18:09:11 +01:00
49ada059bc chore(release): 0.7.31 2025-02-11 18:22:37 +01:00
8003d3eb1e chore(release): 0.7.31-beta.5 2025-02-09 10:19:09 +01:00
48cfa67889 perf: improve button styles of notes 2025-02-09 10:17:38 +01:00
9cda38e9d1 perf(MySQL): long loading in table settings when no checks present 2025-02-06 18:25:27 +01:00
72f8d4249f fix: improve BLOB primary fields management, fixes #938 2025-02-06 13:28:12 +01:00
0f93d70417 fix: unable to delete rows from context menu 2025-02-06 13:27:02 +01:00
580bef76ba fix: replace 'this.addNotification' with 'addNotification' in useResultTables.ts 2025-02-06 13:25:57 +01:00
7595e89223 fix(devtoolsInstaller): improve file path handling and increase chromium version 2025-02-06 12:50:45 +01:00
7af44d4a2c refactor: add ciaplu for pattern matching in language detection and MIME type resolution 2025-02-05 15:34:01 +01:00
0479e5307c fix(Linux): restored AppImage auto updates 2025-02-03 18:14:38 +01:00
d03c1b90ce chore(release): 0.7.31-beta.4 2025-01-31 18:06:58 +01:00
d34e56a517 fix(Linux): missing window management icons 2025-01-31 18:06:16 +01:00
0f35814ca0 chore(release): 0.7.31-beta.3 2025-01-31 17:54:25 +01:00
96ae09feca feat: implement a better query splitter for SQL queries, fixes #926 2025-01-31 17:28:58 +01:00
e3b30359bf refactor: disable auto opening dev tools in development mode 2025-01-31 13:33:42 +01:00
f3c3284fd1 ci: add GitHub Actions workflow for creating Windows APPX artifacts 2025-01-31 13:32:44 +01:00
27387f18a1 fix(MySQL): adjust utf8mb3 encoding to resolve compatibility issue, fixes #646 2025-01-31 13:32:06 +01:00
8e54f7b801 feat(Linux): update title bar for better Linux experience 2025-01-30 18:01:56 +01:00
70aae2f194 chore(release): 0.7.31-beta.2 2025-01-30 16:16:14 +01:00
592d7b3517 feat: add developer tools and refresh buttons to console in development mode 2025-01-23 18:10:37 +01:00
2b743a2c79 Merge pull request #925 from dyaskur/develop
fix: fail to fill cell to datetime column(Postgre) fixes #924
2025-01-23 09:17:40 +01:00
️Yaskur Dyas⚔⚔️⚔
e493db5112 fix: reorder condition when format the update data 2025-01-23 08:16:52 +07:00
️Yaskur Dyas⚔⚔️⚔
d3d7ab38c0 fix: fail to fill cell to datetime column(Postgre) fixes #924 2025-01-23 06:32:22 +07:00
7a66c11868 chore(release): 0.7.31-beta.1 2025-01-22 10:56:01 +01:00
8544bb5378 refactor: reorder import statements in sqlUtils.ts for consistency 2025-01-22 10:53:58 +01:00
6709a75298 Merge pull request #921 from curiouslad/develop
Zoom in/out and fullscreen shortcuts
2025-01-20 09:07:08 +01:00
f25f6659d5 refactor: enhancement of new shortcuts implementation 2025-01-17 13:45:46 +01:00
8d0ff4953e Merge pull request #922 from jimcat8/cn_trans
Update localization
2025-01-16 09:45:33 +01:00
tianci
fbe28f0ff0 Update localization 2025-01-16 12:22:07 +08:00
0d8bcf5cd6 Merge pull request #920 from dyaskur/develop
fix: Cannot update column value with composite primary key and JSON column #916
2025-01-15 09:12:26 +01:00
mladen
47ac729d2f feat: zoom in/out and fullscreen shortcuts 2025-01-14 05:24:33 +01:00
️Yaskur Dyas⚔⚔️⚔
450c4c47f3 refactor: improve update cell condition and move whereJson formatter to sqlUtils 2025-01-14 05:44:12 +07:00
️Yaskur Dyas⚔⚔️⚔
110dcd335a fix: cannot update on JSON column in MariaDB and PostgreSQL 2025-01-14 05:15:46 +07:00
️Yaskur Dyas⚔⚔️⚔
0029967619 fix: cannot update column value with composite primary key and JSON column, fixes #916 2025-01-14 04:44:29 +07:00
34848e8dc3 Merge branch 'develop' of https://github.com/antares-sql/antares into develop 2025-01-13 10:23:23 +01:00
c32add76e8 Merge pull request #919 from dyaskur/develop
fix: fail to duplicate JSON row
2025-01-13 10:23:17 +01:00
️Yaskur Dyas⚔⚔️⚔
507dc7d55b fix: fail to duplicate JSON row 2025-01-12 10:30:31 +07:00
4a2b5926f4 fix: saved connections lost opening a second window after first app run 2025-01-10 18:20:36 +01:00
ed90b12a7b Merge pull request #918 from antares-sql/all-contributors/add-JoseGonzalez84
docs: add JoseGonzalez84 as a contributor for translation
2025-01-10 08:46:59 +01:00
allcontributors[bot]
00ce76a12e docs: update .all-contributorsrc [skip ci] 2025-01-10 07:46:47 +00:00
allcontributors[bot]
77b3a8a354 docs: update README.md [skip ci] 2025-01-10 07:46:46 +00:00
d3ae45ec94 perf(translation): update spanish translation 2025-01-10 08:45:58 +01:00
ad4478a822 Merge pull request #917 from antares-sql/all-contributors/add-salvymc
docs: add salvymc as a contributor for code
2025-01-09 16:50:59 +01:00
allcontributors[bot]
ba5dd9ff15 docs: update .all-contributorsrc [skip ci] 2025-01-09 15:50:45 +00:00
allcontributors[bot]
5aab824fe9 docs: update README.md [skip ci] 2025-01-09 15:50:44 +00:00
87ab58c50f Merge pull request #912 from salvymc/patch-1
Update WorkspaceExploreBarSchema.vue - Changed search to not be case sensitive
2025-01-09 16:48:37 +01:00
e986f287c6 chore(release): 0.7.31-beta.0 2025-01-06 11:11:23 +01:00
Salvatore Forino
39a30e48dd Update WorkspaceExploreBarSchema.vue
Changed search to not be case sensitive
2025-01-03 13:09:15 +01:00
46165d2f4f Merge pull request #910 from antares-sql/all-contributors/add-r4f4dev
docs: add r4f4dev as a contributor for translation
2024-12-28 23:46:00 +01:00
allcontributors[bot]
d0e56e4eb6 docs: update .all-contributorsrc [skip ci] 2024-12-28 22:45:32 +00:00
allcontributors[bot]
c803c072d1 docs: update README.md [skip ci] 2024-12-28 22:45:31 +00:00
232211811b Merge pull request #909 from r4f4dev/feat/locale-uzbek
feat(language): add uzbek language support
2024-12-28 15:22:23 +01:00
r4f4dev
fb9c258cc1 feat(language): add uzbek language support 2024-12-28 19:15:41 +05:00
8de99dae7b fix: prevent delete confirmation modal from triggering on non-delete key presses, fixes #906 2024-12-24 11:03:27 +01:00
2bd69c6263 refactor: removed software-side sorting logic, fixes #904 2024-12-24 09:54:47 +01:00
4d1a81033d chore(release): 0.7.30 2024-12-04 18:14:31 +01:00
5887eea2ed Merge branch 'master' of https://github.com/antares-sql/antares 2024-12-04 18:14:11 +01:00
6c69583c90 Merge pull request #899 from antares-sql/all-contributors/add-carvalhods
docs: add carvalhods as a contributor for platform
2024-11-22 09:16:26 +01:00
allcontributors[bot]
03461522b7 docs: update .all-contributorsrc [skip ci] 2024-11-22 08:13:36 +00:00
allcontributors[bot]
11807e3bb6 docs: update README.md [skip ci] 2024-11-22 08:13:35 +00:00
a54b8d719c fix: issue saving queries as file 2024-11-21 13:28:33 +01:00
d1f68da495 chore(release): 0.7.30-beta.1 2024-11-15 14:27:20 +01:00
d666281daa Update README.md 2024-11-11 15:50:55 +01:00
481ae842dd Merge pull request #894 from leaked/master
Update README.md
2024-11-11 15:49:43 +01:00
Mohsen Nasiri
acf5d459e2 Update README.md 2024-11-10 02:47:52 +03:30
2ee9cfcf0b fix: missing support check for table check features 2024-11-08 18:12:02 +01:00
f0d312fb59 perf(PostgreSQL): improved support of connection strings, closes #893 2024-11-08 18:09:37 +01:00
c97ade949c chore(release): 0.7.30-beta.0 2024-10-25 18:46:27 +02:00
38af648440 refactor: ts fix 2024-10-25 18:44:49 +02:00
ccbcffc7f0 Merge pull request #890 from antares-sql/feat/mysql-check-support
Feat: MySQL check support
2024-10-25 18:34:06 +02:00
dfa7cf9905 perf: added more notifications in debug console 2024-10-25 18:32:07 +02:00
6365e07534 feat(MySQL): check constraints management support 2024-10-25 18:30:34 +02:00
24605d01e1 Merge pull request #887 from antares-sql/all-contributors/add-SawGoD
docs: add SawGoD as a contributor for translation
2024-10-22 15:53:15 +02:00
f083a8a185 Merge pull request #886 from SawGoD/master
feat: update ru-RU.ts file and update translation
2024-10-22 15:52:58 +02:00
allcontributors[bot]
f639bc7983 docs: update .all-contributorsrc [skip ci] 2024-10-22 13:26:01 +00:00
allcontributors[bot]
5e51997e5b docs: update README.md [skip ci] 2024-10-22 13:26:00 +00:00
SawGoD
1e3c9edb50 Update ru-RU.ts file and update translation 2024-10-22 11:41:36 +03:00
60e1e59505 fix: incorrect behavior sorting tables with numeric values 2024-10-18 18:24:46 +02:00
dba490f226 fix(MySQL): routines do not return results, fixes #885 2024-10-17 18:14:41 +02:00
b6c5dff15c fix: incorrect behavior in sorting tables with null/empty values, fixes #883 2024-10-17 18:12:34 +02:00
d2da8c2446 chore(release): 0.7.29 2024-10-14 09:40:47 +02:00
b54d2c9f5e chore(release): 0.7.29-beta.3 2024-10-08 18:38:02 +02:00
9a0ad80bb5 perf(MySQL): made some common errors related to corrupted tables non-blocking, closes #877 2024-10-08 18:34:29 +02:00
2f3f5de8d6 feat(translation): add hebrew translation, closes #878 2024-10-08 18:26:30 +02:00
b2c046fd38 Merge pull request #879 from antares-sql/all-contributors/add-LeviEyal
docs: add LeviEyal as a contributor for translation
2024-10-08 12:47:01 +02:00
allcontributors[bot]
bbc29a6335 docs: update .all-contributorsrc [skip ci] 2024-10-08 10:46:28 +00:00
allcontributors[bot]
b6c337638c docs: update README.md [skip ci] 2024-10-08 10:46:27 +00:00
2120a59d41 chore(release): 0.7.29-beta.2 2024-10-02 10:18:14 +02:00
2cda4a1fa1 fix(MySQL): missing exported values for DEFAULT_GENERATED table fields, fixes #854 2024-10-01 18:08:58 +02:00
76c8cd1beb Merge pull request #875 from mirrorb/master
fix(PostgreSQL): unable to change table comment to empty, error changing the comment for a specific table name
2024-09-30 18:12:02 +02:00
14aeebed9c feat(UI): new context menu and some minor improvements to query tabs, closes #867 2024-09-30 18:10:38 +02:00
mirrorb
1a1118452a Merge remote-tracking branch 'upstream/develop' 2024-09-30 11:22:46 +08:00
mirrorb
4b0f596405 Merge branch 'master' of https://github.com/antares-sql/antares 2024-09-30 11:05:02 +08:00
mirrorb
eb749f0f66 fix(PostgreSQL): error changing the comment for a specific table name 2024-09-30 11:02:26 +08:00
mirrorb
d78e59dd09 fix(PostgreSQL): unable to change table comment to empty 2024-09-30 10:57:19 +08:00
7969294a93 fix(MySQL): incorrect representation of the DATE if the year is prior to 1900, fixes #860 2024-09-29 13:50:22 +02:00
2ae016f0b6 chore(release): 0.7.29-beta.1 2024-09-28 15:50:38 +02:00
b4f33bc474 Merge branch 'develop' of https://github.com/antares-sql/antares into beta 2024-09-28 15:50:09 +02:00
f185463866 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2024-09-28 15:46:48 +02:00
3fa0bd3cd1 Merge pull request #873 from antares-sql/all-contributors/add-mirrorb
docs: add mirrorb as a contributor for code
2024-09-28 15:46:16 +02:00
allcontributors[bot]
0d3ef39822 docs: update .all-contributorsrc [skip ci] 2024-09-28 13:46:03 +00:00
allcontributors[bot]
a02913f4e5 docs: update README.md [skip ci] 2024-09-28 13:46:02 +00:00
f9f993cbcd Merge pull request #872 from mirrorb/master
feat(PostgreSQL): table and field comments
2024-09-28 15:43:05 +02:00
mirrorb
ebd1a75445 feat(PostgreSQL): table and field comments 2024-09-27 17:43:54 +08:00
4201532081 Merge pull request #870 from antares-sql/all-contributors/add-zwei-c
docs: add zwei-c as a contributor for translation
2024-09-25 09:19:44 +02:00
allcontributors[bot]
c5cb586358 docs: update .all-contributorsrc [skip ci] 2024-09-25 07:19:18 +00:00
allcontributors[bot]
c111b2c0f5 docs: update README.md [skip ci] 2024-09-25 07:19:17 +00:00
b70ed124eb feat(translation): traditional chinese translation, closes #869 2024-09-25 09:18:37 +02:00
010147b553 chore(release): 0.7.29-beta.0 2024-09-23 09:12:15 +02:00
da8cc39157 fix: mismatch between table field columns and results with duplicate fields, fixes #848 2024-09-20 18:08:11 +02:00
37d44c95ee perf(UI): hide edit/delete functions in readonly mode 2024-09-18 16:42:21 +02:00
4df4c6197d Merge pull request #858 from Lawondyss/lawondyss
feat: update czech translation
2024-08-30 16:25:29 +02:00
Ladislav Vondráček
0506b653d7 feat: update czech translation 2024-08-30 15:22:19 +02:00
5fd9fe48a2 Merge pull request #853 from hatch01/master
build(deps): update various dependencies
2024-08-30 09:07:42 +02:00
b6a7124f33 feat: cancel button when waiting to connect database, closes #830 2024-08-28 18:05:04 +02:00
eymeric
5855ab0921 build(deps): update various dependencies 2024-08-26 22:15:47 +02:00
c2b602785a chore(release): 0.7.28 2024-08-20 17:48:59 +02:00
97279742e9 chore(release): 0.7.28-beta.0 2024-08-06 18:45:16 +02:00
6cb21ff792 fix: html tags searching in history or saved queries, fixes #847 2024-08-05 18:03:10 +02:00
72bacdeabf fix: disabled column sort during loadings 2024-08-05 18:02:24 +02:00
c434855879 fix(PostgreSQL): issue exporting tables with primary keys 2024-07-30 16:52:20 +02:00
ba0ffcc6f5 fix: wrong password message importing app data 2024-07-19 18:00:52 +02:00
8e7965a0f9 fix(PostgreSQL): wrong export formato of JSON fields 2024-07-19 18:00:13 +02:00
4aab84fbd5 Merge pull request #839 from 64knl/feat-update-dutch-string
feat: add missing Dutch strings
2024-07-18 09:15:00 +02:00
Rene
74e97e660d feat(translation): Add more faker translations 2024-07-16 14:56:15 +02:00
Rene
f5d236b521 fix(translation): Spelling error 2024-07-16 14:49:21 +02:00
Rene
e794d207ad feat: add missing Dutch strings 2024-07-16 14:36:35 +02:00
f99a3cc054 chore(release): 0.7.27 2024-07-16 14:01:25 +02:00
59f7d3c670 fix: issue with new console and languages different than english, fixes #837 2024-07-16 14:00:02 +02:00
8f2daa0f1c chore(release): 0.7.26 2024-07-15 10:23:38 +02:00
141d5bb69c chore(release): 0.7.26-beta.1 2024-07-11 18:09:01 +02:00
171b6f924a feat: custom SVG icons for connections, closes #663 2024-07-11 18:06:41 +02:00
f7419d8e9c fix: table name in column list on export as SQL features, fixes #822 2024-07-04 18:01:29 +02:00
5b1cd70e25 chore(release): 0.7.26-beta.0 2024-07-01 19:44:25 +02:00
3e223b475e feat: in-app debug console, closes #824 2024-07-01 18:16:18 +02:00
4a38656b7e fix(PostgreSQL): issue with ssl enabled and connection strings, fixes #786 2024-06-21 08:55:29 +02:00
25b7ae57c6 perf(UI): improved all connections modal, closes #761 2024-06-20 18:12:47 +02:00
4b5718e9b7 ci: update gh actions dependencies 2024-06-20 08:17:09 +02:00
780d83deaa Merge branch 'master' of https://github.com/antares-sql/antares into develop 2024-06-19 14:16:34 +02:00
e952f9f5f8 ci: fix macos build actions 2024-06-19 09:08:25 +02:00
49f1a8ef2e chore(release): 0.7.25 2024-06-19 08:57:50 +02:00
121aa21a6d Merge branch 'beta' of https://github.com/antares-sql/antares 2024-06-19 08:57:07 +02:00
cb25963a67 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2024-06-17 09:26:41 +02:00
3a47607a5f Merge branch 'master' of https://github.com/antares-sql/antares 2024-06-17 09:13:55 +02:00
7494ff6fcf ci: changes on create-artifact-macos.yml 2024-06-17 09:13:31 +02:00
838491bfd4 chore(release): 0.7.25-beta.2 2024-06-16 13:40:02 +02:00
0b9898f3e7 feat(PostgreSQL): support to materialized views, closes #804 2024-06-14 18:05:29 +02:00
a973ec3c60 perf(UI): views grouped in folders 2024-06-13 18:07:05 +02:00
d0c50f17ca ci: temporary disabled auto test e2e 2024-06-10 08:52:00 +02:00
b4cdd58973 chore(release): 0.7.25-beta.1 2024-06-08 17:29:47 +02:00
9947479fdc Merge branch 'beta' of https://github.com/antares-sql/antares into develop 2024-06-07 16:12:10 +02:00
4a1697d633 fix: issue switching table after using a filter, fixes#691 2024-06-05 18:34:43 +02:00
b7dfd5cb8c build(deps): update various dependencies 2024-05-26 17:28:42 +02:00
0ec9d3cfc1 Merge pull request #807 from antares-sql/all-contributors/add-mangas
docs: add mangas as a contributor for code
2024-05-26 17:20:52 +02:00
allcontributors[bot]
4f615b26cf docs: update .all-contributorsrc [skip ci] 2024-05-26 15:19:12 +00:00
allcontributors[bot]
86a1e05197 docs: update README.md [skip ci] 2024-05-26 15:19:11 +00:00
3fa9873d20 Merge pull request #805 from mangas/upgrade-electron
chore: upgrade electron
2024-05-26 17:18:21 +02:00
37a160a03f build(deps): update @electron/remote 2024-05-26 17:15:11 +02:00
2385a8207c chore(release): 0.7.25-beta.0 2024-05-26 15:57:02 +02:00
Filipe Azevedo
243984e697 chore: upgrade electron 2024-05-17 12:46:58 +01:00
d1bb50b2bb fix(PostgreSQL): unable to search for databases, fixes #798 2024-05-08 08:56:23 +02:00
8501fa2e81 Merge branch 'develop' of https://github.com/antares-sql/antares into develop 2024-05-07 18:16:30 +02:00
25123e34ef fix: missing resizebars on mouse over 2024-05-07 18:15:32 +02:00
bd4502ee47 Merge pull request #800 from antares-sql/all-contributors/add-penguinlab
docs: add penguinlab as a contributor for translation
2024-05-07 16:31:40 +02:00
b2f9d475a2 Merge pull request #799 from penguinlab/master
feat: update japanese translation
2024-05-07 16:30:37 +02:00
allcontributors[bot]
93de974b09 docs: update .all-contributorsrc [skip ci] 2024-05-07 14:29:50 +00:00
allcontributors[bot]
949bf4cbcb docs: update README.md [skip ci] 2024-05-07 14:29:49 +00:00
penguinlab
bb3c87b2cf feat: update japanese translation 2024-05-07 18:18:45 +09:00
40bf9a040a Merge pull request #797 from jimcat8/cn_trans
Update zh-CN.ts file and update translation
2024-05-05 20:56:29 +02:00
tianci
978b55fdb1 Update again 2024-05-05 18:11:22 +08:00
tianci
098d4e96d6 Update zh-CN.ts file and update translation 2024-05-05 18:07:02 +08:00
957cb9e1a5 chore(release): 0.7.24 2024-05-03 14:21:14 +02:00
09c274a724 fix: missing accent color change 2024-05-02 18:00:07 +02:00
9bcd874e80 chore(release): 0.7.24-beta.1 2024-04-30 18:09:52 +02:00
ece2ee05cc perf(UI): improvements on light theme 2024-04-30 18:08:07 +02:00
058fc2fc0b feat: accent color based on folder color, closes #762 2024-04-30 18:07:08 +02:00
33bbc0e7e6 fix(PostgreSQL): better handle connection errors, should fix #794 2024-04-30 18:06:11 +02:00
23c59b4d4e fix(PostgreSQL): issue with similar tabs on differend databases 2024-04-18 18:22:29 +02:00
6600197b82 perf(UI): hide "insert row" button in read-only mode, closes #695 2024-04-14 16:23:56 +02:00
33203aeb04 refactor(UI): change query tab buttons order 2024-04-12 18:03:17 +02:00
f4f385589f chore(release): 0.7.24-beta.0 2024-04-12 08:44:08 +02:00
0565ae1204 fix(translation): missing translation for "Open notes" shortcut 2024-04-08 18:33:37 +02:00
258fbc81f7 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2024-04-08 18:30:23 +02:00
8d8650fbe7 feat: unsaved file reminder closing file tabs 2024-04-08 18:29:05 +02:00
af2812f2b0 Merge pull request #788 from antares-sql/all-contributors/add-bagusindrayana
docs: add bagusindrayana as a contributor for code
2024-04-08 12:49:46 +02:00
allcontributors[bot]
d163cbfac8 docs: update .all-contributorsrc [skip ci] 2024-04-08 10:49:32 +00:00
allcontributors[bot]
4537d96f3e docs: update README.md [skip ci] 2024-04-08 10:49:31 +00:00
099a71a189 Merge pull request #785 from bagusindrayana/feat-open-edit-save-file
Feat open, edit, and save file in query tab
2024-04-08 12:49:01 +02:00
e7efb9c616 refactor(UI): change to query tab icons to avoid ambiguity with new features 2024-04-08 09:52:46 +02:00
a752dcb6a9 chore(release): 0.7.23 2024-04-07 16:54:05 +02:00
bagusindrayana
c1e58eb695 feat: open,save, and save as file in query tab 2024-04-06 15:34:42 +08:00
bagusindrayana
f7204dc0ae feat: add translation for open,save, and save as file 2024-04-06 15:34:18 +08:00
bagusindrayana
6b56c60b68 feat: add shortcut open,save, and save as file 2024-04-06 15:33:01 +08:00
1875e895ae chore(release): 0.7.23-beta.1 2024-04-02 09:10:17 +02:00
2064294119 feat: add the page reference in the export file name, closes #772 2024-03-25 09:08:30 +01:00
62e3115860 feat: move connections out of folder from context menu, related to #773 2024-03-24 11:10:00 +01:00
9aef287a98 feat: move connections to folders from context menu, related to #773 2024-03-23 18:45:38 +01:00
65ec4c5da6 fix: bad format of timestamp fields on CSV export, fixes 776 2024-03-23 16:33:19 +01:00
e19118982b chore(release): 0.7.23-beta.0 2024-03-21 23:09:09 +01:00
11f130d91c Merge pull request #778 from dyaskur/fix_shortcut_on_macos
fix: shortcut not working on mac os
2024-03-14 09:05:07 +01:00
Yaskur
0bb5cedda6 fix: shortcut not working on mac os 2024-03-13 15:48:59 +07:00
de9dac3e8a fix: query result sort not working with aliased tables, fixes #765 2024-03-10 16:04:24 +01:00
dd5b41716a fix: CSV export does not escape strings when needed, fixes #770 2024-03-09 15:42:36 +01:00
86acb390ac build: add husky and commitlint 2024-03-09 15:05:55 +01:00
2884ec3dd6 chore(release): 0.7.22 2024-02-26 18:20:31 +01:00
b542a09c01 Merge branch 'beta' of https://github.com/antares-sql/antares 2024-02-26 18:19:35 +01:00
6d94a04b67 Merge branch 'develop' of https://github.com/antares-sql/antares into beta 2024-02-26 18:19:14 +01:00
8500fc40a1 refactor: improved note tab selection 2024-02-26 18:17:15 +01:00
586f901bae fix: delete record modal pressing del when editing a field, fixes #767 2024-02-23 18:08:02 +01:00
04e4d21e20 chore(release): 0.7.22-beta.2 2024-02-18 14:49:58 +01:00
fd3dd03eb2 chore: moved electron in devDependencies 2024-02-18 14:47:56 +01:00
d3f71e65ce feat(MySQL): option to enable single connection mode 2024-02-18 14:37:45 +01:00
90b9b87b1d chore(deps): update various dependencies 2024-02-18 13:44:22 +01:00
e9b42c3edb chore(release): 0.7.22-beta.1 2024-02-12 18:31:01 +01:00
259d051a21 fix: some issues related to previous commit 2024-02-12 18:30:07 +01:00
876d5ea481 perf(MySQL): improvements in connection handling 2024-02-11 16:38:06 +01:00
6d002efaf5 Update FUNDING.yml 2024-02-09 12:07:16 +01:00
58be1abf5f Update README.md 2024-02-09 09:27:16 +01:00
da56905572 refactor: improved SET field edit 2024-02-07 17:37:19 +01:00
d698f2798a fix: unable to edit tables containing SET fields, fixes #755 2024-02-06 18:16:26 +01:00
9a41511c42 Merge pull request #758 from 64knl/feat/translation-spelling
feat: update dutch translation + fix spelling mistake
2024-02-06 16:45:29 +01:00
Rene
30ada13663 feat: update dutch translation + fix spelling mistake 2024-02-06 15:38:42 +01:00
14eeaccb07 chore(release): 0.7.22-beta.0 2024-02-04 14:40:24 +01:00
1a0c5da2f1 feat(UI): resizable textarea in new/edito note, closes #747 2024-02-04 14:38:15 +01:00
bb36e98beb perf(UI): improved notes, fixes #746 2024-01-20 10:11:49 +01:00
8928510fb5 refactor: use Record to type objects 2024-01-19 18:03:20 +01:00
155 changed files with 13947 additions and 10267 deletions

View File

@@ -266,6 +266,105 @@
"contributions": [ "contributions": [
"translation" "translation"
] ]
},
{
"login": "bagusindrayana",
"name": "Bagus Indrayana",
"avatar_url": "https://avatars.githubusercontent.com/u/36830534?v=4",
"profile": "https://github.com/bagusindrayana",
"contributions": [
"code"
]
},
{
"login": "penguinlab",
"name": "Naoki Ishikawa",
"avatar_url": "https://avatars.githubusercontent.com/u/10959317?v=4",
"profile": "https://github.com/penguinlab",
"contributions": [
"translation"
]
},
{
"login": "mangas",
"name": "Filipe Azevedo",
"avatar_url": "https://avatars.githubusercontent.com/u/1640325?v=4",
"profile": "https://fazevedo.dev",
"contributions": [
"code"
]
},
{
"login": "zwei-c",
"name": "CHANG, CHIH WEI",
"avatar_url": "https://avatars.githubusercontent.com/u/55912811?v=4",
"profile": "https://github.com/zwei-c",
"contributions": [
"translation"
]
},
{
"login": "mirrorb",
"name": "GaoChun",
"avatar_url": "https://avatars.githubusercontent.com/u/34116207?v=4",
"profile": "https://github.com/mirrorb",
"contributions": [
"code"
]
},
{
"login": "LeviEyal",
"name": "Eyal Levi",
"avatar_url": "https://avatars.githubusercontent.com/u/48846533?v=4",
"profile": "https://github.com/LeviEyal",
"contributions": [
"translation"
]
},
{
"login": "SawGoD",
"name": "Nikita Karelikov",
"avatar_url": "https://avatars.githubusercontent.com/u/67802757?v=4",
"profile": "http://telegram.dog/SawGoD",
"contributions": [
"translation"
]
},
{
"login": "carvalhods",
"name": "David Carvalho",
"avatar_url": "https://avatars.githubusercontent.com/u/6569255?v=4",
"profile": "https://github.com/carvalhods",
"contributions": [
"platform"
]
},
{
"login": "r4f4dev",
"name": "r4f4dev",
"avatar_url": "https://avatars.githubusercontent.com/u/65920592?v=4",
"profile": "https://github.com/r4f4dev",
"contributions": [
"translation"
]
},
{
"login": "salvymc",
"name": "Salvatore Forino",
"avatar_url": "https://avatars.githubusercontent.com/u/10051897?v=4",
"profile": "https://github.com/salvymc",
"contributions": [
"code"
]
},
{
"login": "JoseGonzalez84",
"name": "José González",
"avatar_url": "https://avatars.githubusercontent.com/u/16820141?v=4",
"profile": "https://gadev.com.es/",
"contributions": [
"translation"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -3,3 +3,4 @@ assets
out out
dist dist
build build
misc

2
.github/FUNDING.yml vendored
View File

@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [fabio286] github: [antares-sql,fabio286]
patreon: #fabio286 patreon: #fabio286
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username

View File

@@ -19,17 +19,19 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: beta ref: beta
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: |
npm i
npm install "dmg-license" --save-optional
- name: "Build" - name: "Build"
run: npm run build run: npm run build

View File

@@ -25,17 +25,19 @@ jobs:
exit 0 exit 0
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: master ref: master
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: |
npm i
npm install "dmg-license" --save-optional
- name: "Build" - name: "Build"
run: npm run build run: npm run build

View File

@@ -25,7 +25,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.

View File

@@ -8,12 +8,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: master ref: master
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
@@ -24,7 +24,7 @@ jobs:
run: npm run build -- --arm64 --linux deb AppImage run: npm run build -- --arm64 --linux deb AppImage
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: linux-build name: linux-build
retention-days: 3 retention-days: 3

View File

@@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
@@ -22,7 +22,7 @@ jobs:
run: npm run build run: npm run build
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: linux-build name: linux-build
retention-days: 3 retention-days: 3

View File

@@ -8,20 +8,21 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- name: npm install & build - name: npm install & build
run: | run: |
npm install npm install
npm install "dmg-license" --save-optional
npm run build npm run build
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: macos-build name: macos-build
retention-days: 3 retention-days: 3
@@ -38,17 +39,18 @@ jobs:
ref: beta ref: beta
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- name: npm install & build - name: npm install & build
run: | run: |
npm install npm install
npm install "dmg-license" --save-optional
npm run build npm run build
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: macos-build-beta name: macos-build-beta
retention-days: 3 retention-days: 3

View File

@@ -0,0 +1,32 @@
name: Create artifact [WINDOWS APPX]
on:
workflow_dispatch: {}
jobs:
build:
runs-on: windows-2022
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm i
- name: "Build"
run: npm run build:appx
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: windows-build
retention-days: 3
path: |
build
!build/*-unpacked
!build/.icon-ico

View File

@@ -11,7 +11,7 @@ jobs:
- name: Install Python - name: Install Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.8' python-version: '3.11'
- name: Install pipx - name: Install pipx
uses: CfirTsabari/actions-pipx@v1 uses: CfirTsabari/actions-pipx@v1
@@ -30,7 +30,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
# - name: Delete old package-lock.json # - name: Delete old package-lock.json
# run: rm package-lock.json # run: rm package-lock.json

View File

@@ -17,12 +17,12 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: develop ref: develop
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20

View File

@@ -1,9 +1,10 @@
name: Test end-to-end name: Test end-to-end
on: on:
push: workflow_dispatch: {}
branches: # push:
- develop # branches:
# - develop
jobs: jobs:
release: release:
@@ -15,10 +16,10 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20

1
.husky/commit-msg Normal file
View File

@@ -0,0 +1 @@
npx --no -- commitlint --edit $1

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npm run lint

View File

@@ -5,14 +5,14 @@
], ],
"fix": true, "fix": true,
"formatter": "verbose", "formatter": "verbose",
"customSyntax": "postcss-html",
"plugins": [ "plugins": [
"stylelint-scss" "stylelint-scss"
], ],
"rules": { "rules": {
"at-rule-no-unknown": null, "at-rule-no-unknown": null,
"no-descending-specificity": null, "no-descending-specificity": null,
"font-family-no-missing-generic-family-keyword": null, "font-family-no-missing-generic-family-keyword": null
"declaration-colon-newline-after": "always-multi-line"
}, },
"syntax": "scss" "syntax": "scss"
} }

View File

@@ -2,6 +2,401 @@
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.7.33](https://github.com/antares-sql/antares/compare/v0.7.32...v0.7.33) (2025-02-14)
### Bug Fixes
* issue with some SSH connections, fixes [#947](https://github.com/antares-sql/antares/issues/947) ([3129bf4](https://github.com/antares-sql/antares/commit/3129bf4baa5e72b1d79df986605fd5fad1dce291))
### [0.7.32](https://github.com/antares-sql/antares/compare/v0.7.31...v0.7.32) (2025-02-14)
### Bug Fixes
* black background with light theme, fixes [#945](https://github.com/antares-sql/antares/issues/945) ([1d7053c](https://github.com/antares-sql/antares/commit/1d7053ce032efec8377d9500f2e24618f6381ab4))
* enhance SVG support in connection customization, fixes [#939](https://github.com/antares-sql/antares/issues/939) ([49a3589](https://github.com/antares-sql/antares/commit/49a3589536d2e75a14125be7b874e29b60fb56c4))
* improve error handling in SSH connection ([704f708](https://github.com/antares-sql/antares/commit/704f70819b21a42194d8f68cf9b58ba337f1ada7))
* **PostgreSQL:** error with materialized view tabs ([41e797f](https://github.com/antares-sql/antares/commit/41e797f9e27db66370d3ae7750c057f708af76f9))
### [0.7.31](https://github.com/antares-sql/antares/compare/v0.7.31-beta.5...v0.7.31) (2025-02-11)
### [0.7.31-beta.5](https://github.com/antares-sql/antares/compare/v0.7.31-beta.4...v0.7.31-beta.5) (2025-02-09)
### Bug Fixes
* **devtoolsInstaller:** improve file path handling and increase chromium version ([7595e89](https://github.com/antares-sql/antares/commit/7595e892238d2a93c454e9c1f236915fb458eed1))
* improve BLOB primary fields management, fixes [#938](https://github.com/antares-sql/antares/issues/938) ([72f8d42](https://github.com/antares-sql/antares/commit/72f8d4249f7f587d3e92b46cf7709ddab42107d4))
* **Linux:** restored AppImage auto updates ([0479e53](https://github.com/antares-sql/antares/commit/0479e5307c9a9d5f791e1c61fa772d331f6f7f1f))
* replace 'this.addNotification' with 'addNotification' in useResultTables.ts ([580bef7](https://github.com/antares-sql/antares/commit/580bef76ba390fc85df0892265f31392b80301bd))
* unable to delete rows from context menu ([0f93d70](https://github.com/antares-sql/antares/commit/0f93d70417871f02f9f64f203f6654fa1bf2004b))
### Improvements
* improve button styles of notes ([48cfa67](https://github.com/antares-sql/antares/commit/48cfa67889bd83228c109b7966c4acea4e542fc6))
* **MySQL:** long loading in table settings when no checks present ([9cda38e](https://github.com/antares-sql/antares/commit/9cda38e9d10e3000473863560d8be8f426a5ed17))
### [0.7.31-beta.4](https://github.com/antares-sql/antares/compare/v0.7.31-beta.3...v0.7.31-beta.4) (2025-01-31)
### Bug Fixes
* **Linux:** missing window management icons ([d34e56a](https://github.com/antares-sql/antares/commit/d34e56a517784dea16a7a53bc2249072a3b96596))
### [0.7.31-beta.3](https://github.com/antares-sql/antares/compare/v0.7.31-beta.2...v0.7.31-beta.3) (2025-01-31)
### Features
* implement a better query splitter for SQL queries, fixes [#926](https://github.com/antares-sql/antares/issues/926) ([96ae09f](https://github.com/antares-sql/antares/commit/96ae09fecad0c1fc8926d5dcf64cc779abe5ed49))
* **Linux:** update title bar for better Linux experience ([8e54f7b](https://github.com/antares-sql/antares/commit/8e54f7b80135768a33934bc9336239dee38401a5))
### Bug Fixes
* **MySQL:** adjust utf8mb3 encoding to resolve compatibility issue, fixes [#646](https://github.com/antares-sql/antares/issues/646) ([27387f1](https://github.com/antares-sql/antares/commit/27387f18a107fc6c09afec5f85134496ce764355))
### [0.7.31-beta.2](https://github.com/antares-sql/antares/compare/v0.7.31-beta.1...v0.7.31-beta.2) (2025-01-30)
### Features
* add developer tools and refresh buttons to console in development mode ([592d7b3](https://github.com/antares-sql/antares/commit/592d7b35170f8437ebc15221c97985e889fccb40))
### Bug Fixes
* fail to fill cell to datetime column(Postgre) fixes [#924](https://github.com/antares-sql/antares/issues/924) ([d3d7ab3](https://github.com/antares-sql/antares/commit/d3d7ab38c029fc5ec23767c6c86c49a96e4e329c))
* reorder condition when format the update data ([e493db5](https://github.com/antares-sql/antares/commit/e493db5112478ff121e4e77f69c21747c5d2e032))
### [0.7.31-beta.1](https://github.com/antares-sql/antares/compare/v0.7.31-beta.0...v0.7.31-beta.1) (2025-01-22)
### Features
* zoom in/out and fullscreen shortcuts ([47ac729](https://github.com/antares-sql/antares/commit/47ac729d2f5cced2c503358f7d45a1795f232a20))
### Bug Fixes
* cannot update column value with composite primary key and JSON column, fixes [#916](https://github.com/antares-sql/antares/issues/916) ([0029967](https://github.com/antares-sql/antares/commit/002996761997444ff689bf2384dae64ccb9ef8f7))
* fail to duplicate JSON row ([507dc7d](https://github.com/antares-sql/antares/commit/507dc7d55b342240bf18fd58e6bc71709e8e33a0))
* saved connections lost opening a second window after first app run ([4a2b592](https://github.com/antares-sql/antares/commit/4a2b5926f4783d0b9b1e28485e9293a25ddd31f3))
### Improvements
* **translation:** update spanish translation ([d3ae45e](https://github.com/antares-sql/antares/commit/d3ae45ec94b3538e84ac3013b285034caea695cf))
### [0.7.31-beta.0](https://github.com/antares-sql/antares/compare/v0.7.30...v0.7.31-beta.0) (2025-01-06)
### Features
* **language:** add uzbek language support ([fb9c258](https://github.com/antares-sql/antares/commit/fb9c258cc10e4d85242ca533a66a95f4101d472c))
### Bug Fixes
* prevent delete confirmation modal from triggering on non-delete key presses, fixes [#906](https://github.com/antares-sql/antares/issues/906) ([8de99da](https://github.com/antares-sql/antares/commit/8de99dae7b6eb72bd6833c607d3c3a5db9508ebb))
### [0.7.30](https://github.com/antares-sql/antares/compare/v0.7.30-beta.1...v0.7.30) (2024-12-04)
### Bug Fixes
* issue saving queries as file ([a54b8d7](https://github.com/antares-sql/antares/commit/a54b8d719c6454500b885050c9ce6feaf7cfae1f))
### [0.7.30-beta.1](https://github.com/antares-sql/antares/compare/v0.7.30-beta.0...v0.7.30-beta.1) (2024-11-15)
### Bug Fixes
* missing support check for table check features ([2ee9cfc](https://github.com/antares-sql/antares/commit/2ee9cfcf0bbcf86e8a194d2eff78801300ce7cb3))
### Improvements
* **PostgreSQL:** improved support of connection strings, closes [#893](https://github.com/antares-sql/antares/issues/893) ([f0d312f](https://github.com/antares-sql/antares/commit/f0d312fb59fd98d6e4501bc407959b91eb0650f2))
### [0.7.30-beta.0](https://github.com/antares-sql/antares/compare/v0.7.29...v0.7.30-beta.0) (2024-10-25)
### Features
* **MySQL:** check constraints management support ([6365e07](https://github.com/antares-sql/antares/commit/6365e075349e00caa1454cce862e918f2069878f))
### Bug Fixes
* incorrect behavior in sorting tables with null/empty values, fixes [#883](https://github.com/antares-sql/antares/issues/883) ([b6c5dff](https://github.com/antares-sql/antares/commit/b6c5dff15c165261e9a11a389ed415e59c7b7628))
* incorrect behavior sorting tables with numeric values ([60e1e59](https://github.com/antares-sql/antares/commit/60e1e595057c3ba7f36e0f829dba11b470e1069b))
* **MySQL:** routines do not return results, fixes [#885](https://github.com/antares-sql/antares/issues/885) ([dba490f](https://github.com/antares-sql/antares/commit/dba490f22634f87d3af5a3a4c0866fc3095c9842))
### Improvements
* added more notifications in debug console ([dfa7cf9](https://github.com/antares-sql/antares/commit/dfa7cf9905a4d0a79eaed823a14477574b329dfa))
### [0.7.29](https://github.com/antares-sql/antares/compare/v0.7.29-beta.3...v0.7.29) (2024-10-14)
### [0.7.29-beta.3](https://github.com/antares-sql/antares/compare/v0.7.29-beta.2...v0.7.29-beta.3) (2024-10-08)
### Features
* **translation:** add hebrew translation, closes [#878](https://github.com/antares-sql/antares/issues/878) ([2f3f5de](https://github.com/antares-sql/antares/commit/2f3f5de8d6b02cfbf5217adfcb09a61e13d1e901))
### Improvements
* **MySQL:** made some common errors related to corrupted tables non-blocking, closes [#877](https://github.com/antares-sql/antares/issues/877) ([9a0ad80](https://github.com/antares-sql/antares/commit/9a0ad80bb55f84bd6c90cc1e9b63b33512d336a8))
### [0.7.29-beta.2](https://github.com/antares-sql/antares/compare/v0.7.29-beta.1...v0.7.29-beta.2) (2024-10-02)
### Features
* **UI:** new context menu and some minor improvements to query tabs, closes [#867](https://github.com/antares-sql/antares/issues/867) ([14aeebe](https://github.com/antares-sql/antares/commit/14aeebed9cd8e475548f5e0ade105f4b11954cb2))
### Bug Fixes
* **MySQL:** incorrect representation of the DATE if the year is prior to 1900, fixes [#860](https://github.com/antares-sql/antares/issues/860) ([7969294](https://github.com/antares-sql/antares/commit/7969294a93a51861c57d4396c7a0d89ecc7e8a84))
* **MySQL:** missing exported values for DEFAULT_GENERATED table fields, fixes [#854](https://github.com/antares-sql/antares/issues/854) ([2cda4a1](https://github.com/antares-sql/antares/commit/2cda4a1fa1c80f3567e160caf0b93bc19d76fbaa))
* **PostgreSQL:** error changing the comment for a specific table name ([eb749f0](https://github.com/antares-sql/antares/commit/eb749f0f66bf6547053e30b1503c8b2990ae5950))
* **PostgreSQL:** unable to change table comment to empty ([d78e59d](https://github.com/antares-sql/antares/commit/d78e59dd0910d3ea6ec5183a8748420b2db57050))
### [0.7.29-beta.1](https://github.com/antares-sql/antares/compare/v0.7.29-beta.0...v0.7.29-beta.1) (2024-09-28)
### Features
* **PostgreSQL:** table and field comments ([ebd1a75](https://github.com/antares-sql/antares/commit/ebd1a7544594eb4498560cc64de4b94146ee8439))
* **translation:** traditional chinese translation, closes [#869](https://github.com/antares-sql/antares/issues/869) ([b70ed12](https://github.com/antares-sql/antares/commit/b70ed124eb753091a6afe637d75e59ee9771c8eb))
### [0.7.29-beta.0](https://github.com/antares-sql/antares/compare/v0.7.28...v0.7.29-beta.0) (2024-09-23)
### Features
* cancel button when waiting to connect database, closes [#830](https://github.com/antares-sql/antares/issues/830) ([b6a7124](https://github.com/antares-sql/antares/commit/b6a7124f33397a2ae7da654b5867f6982ac5810e))
* update czech translation ([0506b65](https://github.com/antares-sql/antares/commit/0506b653d74d8cd5e848bc2ec4d29d4b0247c880))
### Bug Fixes
* mismatch between table field columns and results with duplicate fields, fixes [#848](https://github.com/antares-sql/antares/issues/848) ([da8cc39](https://github.com/antares-sql/antares/commit/da8cc39157a4b507d3d377ee1e888b8f8a52b7c5))
### Improvements
* **UI:** hide edit/delete functions in readonly mode ([37d44c9](https://github.com/antares-sql/antares/commit/37d44c95ee559f3ee1345e91fca5e2c1e86c5fbf))
### [0.7.28](https://github.com/antares-sql/antares/compare/v0.7.28-beta.0...v0.7.28) (2024-08-20)
### [0.7.28-beta.0](https://github.com/antares-sql/antares/compare/v0.7.27...v0.7.28-beta.0) (2024-08-06)
### Features
* add missing Dutch strings ([e794d20](https://github.com/antares-sql/antares/commit/e794d207ad75e0f5380f57df579912893e5edb6a))
* **translation:** Add more faker translations ([74e97e6](https://github.com/antares-sql/antares/commit/74e97e660df089ed8273565942118e112f6b3220))
### Bug Fixes
* disabled column sort during loadings ([72bacde](https://github.com/antares-sql/antares/commit/72bacdeabf833880482a839c4735505573d33bdc))
* html tags searching in history or saved queries, fixes [#847](https://github.com/antares-sql/antares/issues/847) ([6cb21ff](https://github.com/antares-sql/antares/commit/6cb21ff7926c74469b421c47b434612b3894b4c2))
* **PostgreSQL:** issue exporting tables with primary keys ([c434855](https://github.com/antares-sql/antares/commit/c434855879de16f83e17784e38e931decdd94873))
* **PostgreSQL:** wrong export formato of JSON fields ([8e7965a](https://github.com/antares-sql/antares/commit/8e7965a0f94a17ed73d5c8913f66e4e9cf0b11c7))
* **translation:** Spelling error ([f5d236b](https://github.com/antares-sql/antares/commit/f5d236b521a3534754de0b1031513f8eb83b3cc0))
* wrong password message importing app data ([ba0ffcc](https://github.com/antares-sql/antares/commit/ba0ffcc6f56c5506c1768c05d43bb07f7b090a68))
### [0.7.27](https://github.com/antares-sql/antares/compare/v0.7.26...v0.7.27) (2024-07-16)
### Bug Fixes
* issue with new console and languages different than english, fixes [#837](https://github.com/antares-sql/antares/issues/837) ([59f7d3c](https://github.com/antares-sql/antares/commit/59f7d3c67083ac7e32bd29c9b7e6e044f2060c2f))
### [0.7.26](https://github.com/antares-sql/antares/compare/v0.7.26-beta.1...v0.7.26) (2024-07-15)
### [0.7.26-beta.1](https://github.com/antares-sql/antares/compare/v0.7.26-beta.0...v0.7.26-beta.1) (2024-07-11)
### Features
* custom SVG icons for connections, closes [#663](https://github.com/antares-sql/antares/issues/663) ([171b6f9](https://github.com/antares-sql/antares/commit/171b6f924acc7d7696f4f850a704af0baf616b87))
### Bug Fixes
* table name in column list on export as SQL features, fixes [#822](https://github.com/antares-sql/antares/issues/822) ([f7419d8](https://github.com/antares-sql/antares/commit/f7419d8e9c4fe8ea80dbf9b2612ff44a66f50365))
### [0.7.26-beta.0](https://github.com/antares-sql/antares/compare/v0.7.25...v0.7.26-beta.0) (2024-07-01)
### Features
* in-app debug console, closes [#824](https://github.com/antares-sql/antares/issues/824) ([3e223b4](https://github.com/antares-sql/antares/commit/3e223b475ea57b24a6782feeabecad9c5596e271))
### Bug Fixes
* **PostgreSQL:** issue with ssl enabled and connection strings, fixes [#786](https://github.com/antares-sql/antares/issues/786) ([4a38656](https://github.com/antares-sql/antares/commit/4a38656b7e1094d3a9df28ce263c272f2014adb7))
### Improvements
* **UI:** improved all connections modal, closes [#761](https://github.com/antares-sql/antares/issues/761) ([25b7ae5](https://github.com/antares-sql/antares/commit/25b7ae57c6a4be9e825dddc7a52a49b67e03771b))
### [0.7.25](https://github.com/antares-sql/antares/compare/v0.7.25-beta.2...v0.7.25) (2024-06-19)
### [0.7.25-beta.2](https://github.com/antares-sql/antares/compare/v0.7.25-beta.1...v0.7.25-beta.2) (2024-06-16)
### Features
* **PostgreSQL:** support to materialized views, closes [#804](https://github.com/antares-sql/antares/issues/804) ([0b9898f](https://github.com/antares-sql/antares/commit/0b9898f3e714d2cb24d100f55dd3858a644de162))
### Improvements
* **UI:** views grouped in folders ([a973ec3](https://github.com/antares-sql/antares/commit/a973ec3c60398cb16685a4f991c43ec4ee74c986))
### [0.7.25-beta.1](https://github.com/antares-sql/antares/compare/v0.7.25-beta.0...v0.7.25-beta.1) (2024-06-08)
### Bug Fixes
* issue switching table after using a filter, fixes[#691](https://github.com/antares-sql/antares/issues/691) ([4a1697d](https://github.com/antares-sql/antares/commit/4a1697d63351b9990efff5804b95d92ac2fc9783))
### [0.7.25-beta.0](https://github.com/antares-sql/antares/compare/v0.7.24...v0.7.25-beta.0) (2024-05-26)
### Features
* update japanese translation ([bb3c87b](https://github.com/antares-sql/antares/commit/bb3c87b2cf6fa38e3cfb68317c02aa350aae7887))
### Bug Fixes
* missing resizebars on mouse over ([25123e3](https://github.com/antares-sql/antares/commit/25123e34ef860d8bf019c496097e68e0101c9ab9))
* **PostgreSQL:** unable to search for databases, fixes [#798](https://github.com/antares-sql/antares/issues/798) ([d1bb50b](https://github.com/antares-sql/antares/commit/d1bb50b2bb48d3445080990c28fdc656cf27a6d3))
### [0.7.24](https://github.com/antares-sql/antares/compare/v0.7.24-beta.1...v0.7.24) (2024-05-03)
### Bug Fixes
* missing accent color change ([09c274a](https://github.com/antares-sql/antares/commit/09c274a724b5020efc650aaf7eecb2404343a6fc))
### [0.7.24-beta.1](https://github.com/antares-sql/antares/compare/v0.7.24-beta.0...v0.7.24-beta.1) (2024-04-30)
### Features
* accent color based on folder color, closes [#762](https://github.com/antares-sql/antares/issues/762) ([058fc2f](https://github.com/antares-sql/antares/commit/058fc2fc0b34cde5aa19233a4a999ef3624dae71))
### Bug Fixes
* **PostgreSQL:** better handle connection errors, should fix [#794](https://github.com/antares-sql/antares/issues/794) ([33bbc0e](https://github.com/antares-sql/antares/commit/33bbc0e7e6be370c944e979a34ab2cb19562d1e3))
* **PostgreSQL:** issue with similar tabs on differend databases ([23c59b4](https://github.com/antares-sql/antares/commit/23c59b4d4e8f250acad75f54d157c7c162e1c4f8))
### Improvements
* **UI:** hide "insert row" button in read-only mode, closes [#695](https://github.com/antares-sql/antares/issues/695) ([6600197](https://github.com/antares-sql/antares/commit/6600197b8286ced4c79378883594d21e69a83d8c))
* **UI:** improvements on light theme ([ece2ee0](https://github.com/antares-sql/antares/commit/ece2ee05cc90a58c1926e882e3ccf4f057f02d68))
### [0.7.24-beta.0](https://github.com/antares-sql/antares/compare/v0.7.23...v0.7.24-beta.0) (2024-04-12)
### Features
* add shortcut open,save, and save as file ([6b56c60](https://github.com/antares-sql/antares/commit/6b56c60b68647bc7182548a137cccc3413e3fbd5))
* add translation for open,save, and save as file ([f7204dc](https://github.com/antares-sql/antares/commit/f7204dc0ae721534eaefbde097d1c26c1d72ad41))
* open,save, and save as file in query tab ([c1e58eb](https://github.com/antares-sql/antares/commit/c1e58eb695de78fbf1d2b26c608692f0962373df))
* unsaved file reminder closing file tabs ([8d8650f](https://github.com/antares-sql/antares/commit/8d8650fbe76c79fd66be857d049b3baaa9ab1f9f))
### Bug Fixes
* **translation:** missing translation for "Open notes" shortcut ([0565ae1](https://github.com/antares-sql/antares/commit/0565ae12042901b9d67fe3e0ea269562ec444994))
### [0.7.23](https://github.com/antares-sql/antares/compare/v0.7.23-beta.1...v0.7.23) (2024-04-07)
### [0.7.23-beta.1](https://github.com/antares-sql/antares/compare/v0.7.23-beta.0...v0.7.23-beta.1) (2024-04-02)
### Features
* add the page reference in the export file name, closes [#772](https://github.com/antares-sql/antares/issues/772) ([2064294](https://github.com/antares-sql/antares/commit/2064294119ed9dfab2a9968dfb5b35d52e2ae03b))
* move connections out of folder from context menu, related to [#773](https://github.com/antares-sql/antares/issues/773) ([62e3115](https://github.com/antares-sql/antares/commit/62e311586073ae7ee4896305198c7168f637c1af))
* move connections to folders from context menu, related to [#773](https://github.com/antares-sql/antares/issues/773) ([9aef287](https://github.com/antares-sql/antares/commit/9aef287a983754158cdbdc9b2a72db9ab82f76c8))
### Bug Fixes
* bad format of timestamp fields on CSV export, fixes 776 ([65ec4c5](https://github.com/antares-sql/antares/commit/65ec4c5da6187a7ab2dfff59326cd12bfa788c3b))
### [0.7.23-beta.0](https://github.com/antares-sql/antares/compare/v0.7.22...v0.7.23-beta.0) (2024-03-21)
### Bug Fixes
* CSV export does not escape strings when needed, fixes [#770](https://github.com/antares-sql/antares/issues/770) ([dd5b417](https://github.com/antares-sql/antares/commit/dd5b41716a10cf9500f2c611b232f5b5b0756a68))
* query result sort not working with aliased tables, fixes [#765](https://github.com/antares-sql/antares/issues/765) ([de9dac3](https://github.com/antares-sql/antares/commit/de9dac3e8abf3b3261f8c54c88cf2386a5be2207))
* shortcut not working on mac os ([0bb5ced](https://github.com/antares-sql/antares/commit/0bb5cedda6a67ccbeea8c127b799f533395101a2))
### [0.7.22](https://github.com/antares-sql/antares/compare/v0.7.22-beta.2...v0.7.22) (2024-02-26)
### Bug Fixes
* delete record modal pressing del when editing a field, fixes [#767](https://github.com/antares-sql/antares/issues/767) ([586f901](https://github.com/antares-sql/antares/commit/586f901bae9a80c0e53ac1d804cbae3f05e26d8e))
### [0.7.22-beta.2](https://github.com/antares-sql/antares/compare/v0.7.22-beta.1...v0.7.22-beta.2) (2024-02-18)
### Features
* **MySQL:** option to enable single connection mode ([d3f71e6](https://github.com/antares-sql/antares/commit/d3f71e65cef88838f03f95a4b34e197fb61878f8))
### [0.7.22-beta.1](https://github.com/antares-sql/antares/compare/v0.7.22-beta.0...v0.7.22-beta.1) (2024-02-12)
### Features
* update dutch translation + fix spelling mistake ([30ada13](https://github.com/antares-sql/antares/commit/30ada13663e88f89beb3dd7291010837059585d5))
### Bug Fixes
* some issues related to previous commit ([259d051](https://github.com/antares-sql/antares/commit/259d051a21e334496d3a52b662f1855ba9a9046d))
* unable to edit tables containing SET fields, fixes [#755](https://github.com/antares-sql/antares/issues/755) ([d698f27](https://github.com/antares-sql/antares/commit/d698f2798a2423f86e6d786dd3ab80439b372a08))
### Improvements
* **MySQL:** improvements in connection handling ([876d5ea](https://github.com/antares-sql/antares/commit/876d5ea48185334e9e2fc981c4282a9c42d22b10))
### [0.7.22-beta.0](https://github.com/antares-sql/antares/compare/v0.7.21...v0.7.22-beta.0) (2024-02-04)
### Features
* **UI:** resizable textarea in new/edito note, closes [#747](https://github.com/antares-sql/antares/issues/747) ([1a0c5da](https://github.com/antares-sql/antares/commit/1a0c5da2f14b99d6f5581b2bf6e916d67d097245))
### Improvements
* **UI:** improved notes, fixes [#746](https://github.com/antares-sql/antares/issues/746) ([bb36e98](https://github.com/antares-sql/antares/commit/bb36e98bebc5e1e55735e98d272428df2ab682e8))
### [0.7.21](https://github.com/antares-sql/antares/compare/v0.7.21-beta.1...v0.7.21) (2024-01-13) ### [0.7.21](https://github.com/antares-sql/antares/compare/v0.7.21-beta.1...v0.7.21) (2024-01-13)

View File

@@ -1,13 +1,13 @@
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
<p align="center"> <p align="center">
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/gh-logo.png"> <img width="800" src="https://raw.githubusercontent.com/antares-sql/antares/master/docs/gh-logo.png">
</p> </p>
<!-- markdownlint-restore --> <!-- markdownlint-restore -->
# Antares SQL Client # Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) ![GitHub](https://img.shields.io/github/license/fabio286/antares) ![Test e2e](https://github.com/antares-sql/antares/actions/workflows/test-e2e-win.yml/badge.svg?branch=develop) ![Mastodon Follow](https://img.shields.io/mastodon/follow/%20110860460902482117?domain=https%3A%2F%2Ffosstodon.org&style=social) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet) ![GitHub package.json version](https://img.shields.io/github/package-json/v/antares-sql/antares) ![GitHub](https://img.shields.io/github/license/antares-sql/antares) ![Test e2e](https://github.com/antares-sql/antares/actions/workflows/test-e2e-win.yml/badge.svg?branch=develop) ![Mastodon Follow](https://img.shields.io/mastodon/follow/%20110860460902482117?domain=https%3A%2F%2Ffosstodon.org&style=social) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions. Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
@@ -16,7 +16,7 @@ Our target is to support as many databases as possible, and all major operating
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/antares-sql/antares/releases/latest).
👁 To stay tuned for new releases follow Antares SQL on [Mastodon](https://fosstodon.org/@AntaresSQL). 👁 To stay tuned for new releases follow Antares SQL on [Mastodon](https://fosstodon.org/@AntaresSQL).
🌟 Don't forget to **leave a star** if you appreciate this project. 🌟 Don't forget to **leave a star** if you appreciate this project.
@@ -60,7 +60,7 @@ On Linux you can simply download and run the `.AppImage` distribution, install f
### Windows ### Windows
On Windows you can choose between downloading the app from Microsoft Store or downloading the `.exe` from our [website](https://antares-sql.app/downloads) 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. On Windows you can choose between downloading the app from Microsoft Store or downloading the `.exe` from our [website](https://antares-sql.app/downloads) or [this github repo](https://github.com/antares-sql/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
@@ -71,17 +71,6 @@ On macOS you can run `.dmg` distribution following [this guide](https://support.
[<img height='56' alt='Download on Flathub' src='https://dl.flathub.org/assets/badges/flathub-badge-en.svg'/>](https://flathub.org/apps/it.fabiodistasio.AntaresSQL) [![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from AUR](https://raw.githubusercontent.com/antares-sql/antares/master/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql-bin) [<img src="https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png" style="height: 56px">](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab) [<img height='56' alt='Download on Flathub' src='https://dl.flathub.org/assets/badges/flathub-badge-en.svg'/>](https://flathub.org/apps/it.fabiodistasio.AntaresSQL) [![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from AUR](https://raw.githubusercontent.com/antares-sql/antares/master/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql-bin) [<img src="https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png" style="height: 56px">](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
🚀 **[Other Downloads](https://github.com/antares-sql/antares/releases/latest)** 🚀 **[Other Downloads](https://github.com/antares-sql/antares/releases/latest)**
## Coming soon
This is a roadmap with major features will come in near future.
- Database tools.
- Users management (add/edit/delete).
- More context menu shortcuts.
- More keyboard shortcuts.
- Support for other databases.
- Apple Silicon distribution
## Currently supported ## Currently supported
### Databases ### Databases
@@ -90,6 +79,7 @@ This is a roadmap with major features will come in near future.
- [x] PostgreSQL - [x] PostgreSQL
- [x] SQLite - [x] SQLite
- [x] Firebird SQL - [x] Firebird SQL
- [ ] DuckDB
- [ ] SQL Server - [ ] SQL Server
- [ ] More... - [ ] More...
@@ -109,9 +99,9 @@ This is a roadmap with major features will come in near future.
## How to contribute ## How to contribute
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares) - 🌍 [Translate Antares](https://github.com/antares-sql/antares/wiki/Translate-Antares)
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide) - 📖 [Contributors Guide](https://github.com/antares-sql/antares/wiki/Contributors-Guide)
- 🚧 [Project Board](https://github.com/antares-sql/antares/projects/1) - 🚧 [Project Board](https://github.com/orgs/antares-sql/projects/3/views/2)
## Contributors ✨ ## Contributors ✨
@@ -158,6 +148,21 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lawondyss"><img src="https://avatars.githubusercontent.com/u/272130?v=4?s=100" width="100px;" alt="Ladislav Vondráček"/><br /><sub><b>Ladislav Vondráček</b></sub></a><br /><a href="#translation-Lawondyss" title="Translation">🌍</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Lawondyss"><img src="https://avatars.githubusercontent.com/u/272130?v=4?s=100" width="100px;" alt="Ladislav Vondráček"/><br /><sub><b>Ladislav Vondráček</b></sub></a><br /><a href="#translation-Lawondyss" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvlad"><img src="https://avatars.githubusercontent.com/u/9055134?v=4?s=100" width="100px;" alt="Vladyslav"/><br /><sub><b>Vladyslav</b></sub></a><br /><a href="#translation-zvlad" title="Translation">🌍</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/zvlad"><img src="https://avatars.githubusercontent.com/u/9055134?v=4?s=100" width="100px;" alt="Vladyslav"/><br /><sub><b>Vladyslav</b></sub></a><br /><a href="#translation-zvlad" title="Translation">🌍</a></td>
</tr> </tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bagusindrayana"><img src="https://avatars.githubusercontent.com/u/36830534?v=4?s=100" width="100px;" alt="Bagus Indrayana"/><br /><sub><b>Bagus Indrayana</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=bagusindrayana" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/penguinlab"><img src="https://avatars.githubusercontent.com/u/10959317?v=4?s=100" width="100px;" alt="Naoki Ishikawa"/><br /><sub><b>Naoki Ishikawa</b></sub></a><br /><a href="#translation-penguinlab" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://fazevedo.dev"><img src="https://avatars.githubusercontent.com/u/1640325?v=4?s=100" width="100px;" alt="Filipe Azevedo"/><br /><sub><b>Filipe Azevedo</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=mangas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zwei-c"><img src="https://avatars.githubusercontent.com/u/55912811?v=4?s=100" width="100px;" alt="CHANG, CHIH WEI"/><br /><sub><b>CHANG, CHIH WEI</b></sub></a><br /><a href="#translation-zwei-c" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mirrorb"><img src="https://avatars.githubusercontent.com/u/34116207?v=4?s=100" width="100px;" alt="GaoChun"/><br /><sub><b>GaoChun</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=mirrorb" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LeviEyal"><img src="https://avatars.githubusercontent.com/u/48846533?v=4?s=100" width="100px;" alt="Eyal Levi"/><br /><sub><b>Eyal Levi</b></sub></a><br /><a href="#translation-LeviEyal" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://telegram.dog/SawGoD"><img src="https://avatars.githubusercontent.com/u/67802757?v=4?s=100" width="100px;" alt="Nikita Karelikov"/><br /><sub><b>Nikita Karelikov</b></sub></a><br /><a href="#translation-SawGoD" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carvalhods"><img src="https://avatars.githubusercontent.com/u/6569255?v=4?s=100" width="100px;" alt="David Carvalho"/><br /><sub><b>David Carvalho</b></sub></a><br /><a href="#platform-carvalhods" title="Packaging/porting to new platform">📦</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/r4f4dev"><img src="https://avatars.githubusercontent.com/u/65920592?v=4?s=100" width="100px;" alt="r4f4dev"/><br /><sub><b>r4f4dev</b></sub></a><br /><a href="#translation-r4f4dev" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/salvymc"><img src="https://avatars.githubusercontent.com/u/10051897?v=4?s=100" width="100px;" alt="Salvatore Forino"/><br /><sub><b>Salvatore Forino</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=salvymc" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://gadev.com.es/"><img src="https://avatars.githubusercontent.com/u/16820141?v=4?s=100" width="100px;" alt="José González"/><br /><sub><b>José González</b></sub></a><br /><a href="#translation-JoseGonzalez84" title="Translation">🌍</a></td>
</tr>
</tbody> </tbody>
</table> </table>

33
commitlint.config.js Normal file
View File

@@ -0,0 +1,33 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// TODO Add Scope Enum Here
// 'scope-enum': [2, 'always', ['yourscope', 'yourscope']],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'chore',
'style',
'refactor',
'build',
'ci',
'test',
'revert',
'perf'
]
],
'subject-case': [
2,
'never',
[
'upper-case',
'pascal-case',
'start-case'
]
]
}
};

12963
package-lock.json generated

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.7.21", "version": "0.7.33",
"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",
@@ -25,7 +25,8 @@
"lint": "eslint . --ext .js,.ts,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"", "lint": "eslint . --ext .js,.ts,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.ts,.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",
"prepare": "husky"
}, },
"author": "Fabio Di Stasio <info@fabiodistasio.it>", "author": "Fabio Di Stasio <info@fabiodistasio.it>",
"main": "./dist/main.js", "main": "./dist/main.js",
@@ -118,45 +119,70 @@
} }
}, },
"dependencies": { "dependencies": {
"@electron/remote": "~2.0.1", "@electron/remote": "~2.1.2",
"@fabio286/ssh2-promise": "~1.0.4-b", "@fabio286/ssh2-promise": "~1.0.4-b",
"@faker-js/faker": "~6.1.2", "@faker-js/faker": "~6.1.2",
"@jamescoyle/vue-icon": "~0.1.2", "@jamescoyle/vue-icon": "~0.1.2",
"@mdi/js": "~7.2.96", "@mdi/js": "~7.2.96",
"@turf/helpers": "~6.5.0", "@turf/helpers": "~6.5.0",
"@vue/compiler-sfc": "~3.2.33",
"@vueuse/core": "~10.4.1", "@vueuse/core": "~10.4.1",
"ace-builds": "~1.24.1", "ace-builds": "~1.34.1",
"better-sqlite3": "^8.0.1", "babel-loader": "~8.2.3",
"better-sqlite3": "~10.0.0",
"chalk": "~4.1.2",
"ciaplu": "^2.2.0",
"cpu-features": "^0.0.10",
"cross-env": "~7.0.2",
"css-loader": "~6.5.0",
"electron-log": "~5.0.1", "electron-log": "~5.0.1",
"electron-store": "~8.1.0", "electron-store": "~8.1.0",
"electron-updater": "~4.6.5", "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",
"file-loader": "~6.2.0",
"floating-vue": "~2.0.0-beta.20", "floating-vue": "~2.0.0-beta.20",
"html-webpack-plugin": "~5.5.0",
"json2php": "~0.0.7", "json2php": "~0.0.7",
"leaflet": "~1.7.1", "leaflet": "~1.7.1",
"marked": "~4.0.19", "marked": "~12.0.0",
"moment": "~2.29.4", "mini-css-extract-plugin": "~2.4.5",
"mysql2": "~3.5.2", "moment": "~2.30.1",
"node-firebird": "~1.1.4", "mysql2": "~3.9.7",
"pg": "~8.11.1", "node-firebird": "~1.1.8",
"pg-connection-string": "~2.5.0", "node-loader": "~2.0.0",
"pg": "~8.11.5",
"pg-query-stream": "~4.2.3", "pg-query-stream": "~4.2.3",
"pgsql-ast-parser": "~7.2.1", "pgsql-ast-parser": "~7.2.1",
"pinia": "~2.1.6", "pinia": "~2.1.7",
"postcss-html": "~1.5.0",
"progress-webpack-plugin": "~1.0.12",
"rimraf": "~3.0.2",
"sass": "~1.42.1",
"sass-loader": "~12.3.0",
"source-map-support": "~0.5.20", "source-map-support": "~0.5.20",
"spectre.css": "~0.5.9", "spectre.css": "~0.5.9",
"sql-formatter": "~13.0.0", "sql-formatter": "~13.0.0",
"sql-highlight": "~4.4.0", "sql-highlight": "~4.4.0",
"style-loader": "~3.3.1",
"tree-kill": "~1.2.2",
"ts-loader": "~9.2.8",
"typescript": "~4.6.3",
"unzip-crx-3": "~0.2.0",
"v-mask": "~2.3.0", "v-mask": "~2.3.0",
"vue": "~3.3.4", "vue": "~3.4.27",
"vue-i18n": "~9.2.2", "vue-i18n": "~9.13.1",
"vuedraggable": "~4.1.0" "vue-loader": "~16.8.3",
"vuedraggable": "~4.1.0",
"webpack": "^5.98.0",
"webpack-cli": "~4.9.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "~7.15.7", "@babel/eslint-parser": "~7.15.7",
"@babel/preset-env": "~7.15.8", "@babel/preset-env": "~7.15.8",
"@babel/preset-typescript": "~7.16.7", "@babel/preset-typescript": "~7.16.7",
"@commitlint/cli": "~19.0.3",
"@commitlint/config-conventional": "~19.0.3",
"@playwright/test": "~1.28.1", "@playwright/test": "~1.28.1",
"@types/better-sqlite3": "~7.5.0", "@types/better-sqlite3": "~7.5.0",
"@types/leaflet": "~1.7.9", "@types/leaflet": "~1.7.9",
@@ -166,14 +192,9 @@
"@types/ssh2": "~1.11.6", "@types/ssh2": "~1.11.6",
"@typescript-eslint/eslint-plugin": "~5.18.0", "@typescript-eslint/eslint-plugin": "~5.18.0",
"@typescript-eslint/parser": "~5.18.0", "@typescript-eslint/parser": "~5.18.0",
"@vue/compiler-sfc": "~3.2.33",
"all-contributors-cli": "~6.20.0", "all-contributors-cli": "~6.20.0",
"babel-loader": "~8.2.3", "electron": "~30.0.8",
"chalk": "~4.1.2", "electron-builder": "~24.13.3",
"cross-env": "~7.0.2",
"css-loader": "~6.5.0",
"electron": "~22.3.27",
"electron-builder": "~24.6.4",
"eslint": "~7.32.0", "eslint": "~7.32.0",
"eslint-config-standard": "~16.0.3", "eslint-config-standard": "~16.0.3",
"eslint-plugin-import": "~2.24.2", "eslint-plugin-import": "~2.24.2",
@@ -181,32 +202,16 @@
"eslint-plugin-promise": "~5.2.0", "eslint-plugin-promise": "~5.2.0",
"eslint-plugin-simple-import-sort": "~10.0.0", "eslint-plugin-simple-import-sort": "~10.0.0",
"eslint-plugin-vue": "~8.0.3", "eslint-plugin-vue": "~8.0.3",
"file-loader": "~6.2.0", "husky": "~9.0.11",
"html-webpack-plugin": "~5.5.0",
"mini-css-extract-plugin": "~2.4.5",
"node-loader": "~2.0.0",
"playwright": "~1.28.1", "playwright": "~1.28.1",
"playwright-core": "~1.28.1", "playwright-core": "~1.28.1",
"postcss-html": "~1.5.0",
"progress-webpack-plugin": "~1.0.12",
"rimraf": "~3.0.2",
"sass": "~1.42.1",
"sass-loader": "~12.3.0",
"standard-version": "~9.3.1", "standard-version": "~9.3.1",
"style-loader": "~3.3.1",
"stylelint": "^15.11.0", "stylelint": "^15.11.0",
"stylelint-config-recommended-vue": "~1.5.0", "stylelint-config-recommended-vue": "~1.5.0",
"stylelint-config-standard": "~34.0.0", "stylelint-config-standard": "~34.0.0",
"stylelint-scss": "~5.3.0", "stylelint-scss": "~5.3.0",
"tree-kill": "~1.2.2",
"ts-loader": "~9.2.8",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "~4.6.3",
"unzip-crx-3": "~0.2.0",
"vue-eslint-parser": "~8.3.0", "vue-eslint-parser": "~8.3.0",
"vue-loader": "~16.8.3",
"webpack": "~5.72.0",
"webpack-cli": "~4.9.1",
"webpack-dev-server": "~4.11.1", "webpack-dev-server": "~4.11.1",
"xvfb-maybe": "~0.2.1" "xvfb-maybe": "~0.2.1"
} }

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck // @ts-check
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const https = require('https'); const https = require('https');
@@ -7,13 +7,18 @@ const unzip = require('unzip-crx-3');
const { antares } = require('../package.json'); const { antares } = require('../package.json');
const extensionID = antares.devtoolsId; const extensionID = antares.devtoolsId;
const chromiumVersion = '124';
const destFolder = path.resolve(__dirname, `../misc/${extensionID}`); const destFolder = path.resolve(__dirname, `../misc/${extensionID}`);
const filePath = path.resolve(__dirname, `${destFolder}${extensionID}.crx`); const filePath = path.resolve(__dirname, `${destFolder}/${extensionID}.crx`);
const fileUrl = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${extensionID}%26uc&prodversion=32`; const fileUrl = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${extensionID}%26uc&prodversion=${chromiumVersion}`;
if (!fs.existsSync(destFolder))
fs.mkdirSync(destFolder, { recursive: true });
const fileStream = fs.createWriteStream(filePath); const fileStream = fs.createWriteStream(filePath);
const downloadFile = url => { const downloadFile = url => {
return new Promise((resolve, reject) => { return /** @type {Promise<void>} */(new Promise((resolve, reject) => {
const request = https.get(url); const request = https.get(url);
request.on('response', response => { request.on('response', response => {
@@ -33,7 +38,7 @@ const downloadFile = url => {
}); });
request.on('error', reject); request.on('error', reject);
request.end(); request.end();
}); }));
}; };
(async () => { (async () => {

View File

@@ -19,6 +19,7 @@ export const defaults: Customizations = {
sshConnection: false, sshConnection: false,
fileConnection: false, fileConnection: false,
cancelQueries: false, cancelQueries: false,
singleConnectionMode: false,
// Tools // Tools
processesList: false, processesList: false,
usersManagement: false, usersManagement: false,
@@ -54,6 +55,7 @@ export const defaults: Customizations = {
tableArray: false, tableArray: false,
tableRealCount: false, tableRealCount: false,
tableDuplicate: false, tableDuplicate: false,
tableCheck: false,
viewSettings: false, viewSettings: false,
triggerSettings: false, triggerSettings: false,
triggerFunctionSettings: false, triggerFunctionSettings: false,

View File

@@ -29,6 +29,7 @@ export const customizations: Customizations = {
sslConnection: true, sslConnection: true,
sshConnection: true, sshConnection: true,
cancelQueries: true, cancelQueries: true,
singleConnectionMode: true,
// Tools // Tools
processesList: true, processesList: true,
// Structure // Structure
@@ -46,6 +47,7 @@ export const customizations: Customizations = {
tableTruncateDisableFKCheck: true, tableTruncateDisableFKCheck: true,
tableDuplicate: true, tableDuplicate: true,
tableDdl: true, tableDdl: true,
tableCheck: true,
viewAdd: true, viewAdd: true,
triggerAdd: true, triggerAdd: true,
routineAdd: true, routineAdd: true,

View File

@@ -31,6 +31,7 @@ export const customizations: Customizations = {
schemas: true, schemas: true,
tables: true, tables: true,
views: true, views: true,
materializedViews: true,
triggers: true, triggers: true,
triggerFunctions: true, triggerFunctions: true,
routines: true, routines: true,
@@ -42,6 +43,7 @@ export const customizations: Customizations = {
tableDuplicate: true, tableDuplicate: true,
tableDdl: true, tableDdl: true,
viewAdd: true, viewAdd: true,
materializedViewAdd: true,
triggerAdd: true, triggerAdd: true,
triggerFunctionAdd: true, triggerFunctionAdd: true,
routineAdd: true, routineAdd: true,
@@ -52,6 +54,7 @@ export const customizations: Customizations = {
databaseEdit: false, databaseEdit: false,
tableSettings: true, tableSettings: true,
viewSettings: true, viewSettings: true,
materializedViewSettings: true,
triggerSettings: true, triggerSettings: true,
triggerFunctionSettings: true, triggerFunctionSettings: true,
routineSettings: true, routineSettings: true,
@@ -59,6 +62,7 @@ export const customizations: Customizations = {
indexes: true, indexes: true,
foreigns: true, foreigns: true,
nullable: true, nullable: true,
comment: true,
tableArray: true, tableArray: true,
procedureSql: '$procedure$\r\n\r\n$procedure$', procedureSql: '$procedure$\r\n\r\n$procedure$',
procedureContext: true, procedureContext: true,

View File

@@ -18,7 +18,7 @@ export type Importer = MySQLImporter | PostgreSQLImporter
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface IpcResponse<T = any> { export interface IpcResponse<T = any> {
status: 'success' | 'error'; status: 'success' | 'error' | 'abort';
response?: T; response?: T;
} }
@@ -34,6 +34,7 @@ export interface ClientParams {
| { databasePath: string; readonly: boolean }; | { databasePath: string; readonly: boolean };
poolSize?: number; poolSize?: number;
logger?: () => void; logger?: () => void;
querySplitter?: (sql: string, clieng?: string) => string[];
} }
/** /**
@@ -52,10 +53,12 @@ export interface ConnectionParams {
password: string; password: string;
ask: boolean; ask: boolean;
readonly: boolean; readonly: boolean;
singleConnectionMode: boolean;
ssl: boolean; ssl: boolean;
cert?: string; cert?: string;
key?: string; key?: string;
ca?: string; ca?: string;
connString?: string;
untrustedConnection: boolean; untrustedConnection: boolean;
ciphers?: string; ciphers?: string;
ssh: boolean; ssh: boolean;
@@ -158,6 +161,13 @@ export interface TableForeign {
oldName?: string; oldName?: string;
} }
export interface TableCheck {
// eslint-disable-next-line camelcase
_antares_id?: string;
name: string;
clause: string;
}
export interface CreateTableParams { export interface CreateTableParams {
/** Connection UID */ /** Connection UID */
uid?: string; uid?: string;
@@ -165,6 +175,7 @@ export interface CreateTableParams {
fields: TableField[]; fields: TableField[];
foreigns: TableForeign[]; foreigns: TableForeign[];
indexes: TableIndex[]; indexes: TableIndex[];
checks?: TableCheck[];
options: TableOptions; options: TableOptions;
} }
@@ -192,6 +203,11 @@ export interface AlterTableParams {
changes: TableForeign[]; changes: TableForeign[];
deletions: TableForeign[]; deletions: TableForeign[];
}; };
checkChanges?: {
additions: TableCheck[];
changes: TableCheck[];
deletions: TableCheck[];
};
options: TableOptions; options: TableOptions;
} }
@@ -363,7 +379,7 @@ export interface QueryBuilderObject {
offset: number; offset: number;
join: string[]; join: string[];
update: string[]; update: string[];
insert: {[key: string]: string | boolean | number }[]; insert: Record<string, string | boolean | number>[];
delete: boolean; delete: boolean;
} }

View File

@@ -19,6 +19,7 @@ export interface Customizations {
sshConnection?: boolean; sshConnection?: boolean;
fileConnection?: boolean; fileConnection?: boolean;
cancelQueries?: boolean; cancelQueries?: boolean;
singleConnectionMode?: boolean;
// Tools // Tools
processesList?: boolean; processesList?: boolean;
usersManagement?: boolean; usersManagement?: boolean;
@@ -27,6 +28,7 @@ export interface Customizations {
schemas?: boolean; schemas?: boolean;
tables?: boolean; tables?: boolean;
views?: boolean; views?: boolean;
materializedViews?: boolean;
triggers?: boolean; triggers?: boolean;
triggerFunctions?: boolean; triggerFunctions?: boolean;
routines?: boolean; routines?: boolean;
@@ -41,9 +43,12 @@ export interface Customizations {
tableArray?: boolean; tableArray?: boolean;
tableRealCount?: boolean; tableRealCount?: boolean;
tableTruncateDisableFKCheck?: boolean; tableTruncateDisableFKCheck?: boolean;
tableCheck?: boolean;
tableDdl?: boolean; tableDdl?: boolean;
viewAdd?: boolean; viewAdd?: boolean;
viewSettings?: boolean; viewSettings?: boolean;
materializedViewAdd?: boolean;
materializedViewSettings?: boolean;
triggerAdd?: boolean; triggerAdd?: boolean;
triggerFunctionAdd?: boolean; triggerFunctionAdd?: boolean;
routineAdd?: boolean; routineAdd?: boolean;

View File

@@ -13,7 +13,7 @@ export interface ExportOptions {
includeContent: boolean; includeContent: boolean;
includeDropStatement: boolean; includeDropStatement: boolean;
}[]; }[];
includes: {[key: string]: boolean}; includes: Record<string, boolean>;
outputFormat: 'sql' | 'sql.zip'; outputFormat: 'sql' | 'sql.zip';
outputFile: string; outputFile: string;
sqlInsertAfter: number; sqlInsertAfter: number;

View File

@@ -18,7 +18,7 @@ export interface TableDeleteParams {
primary?: string; primary?: string;
field: string; field: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
rows: {[key: string]: any}; rows: Record<string, any>;
} }
export type TableFilterOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | 'IN' | 'NOT IN' | 'LIKE' | 'NOT LIKE' | 'RLIKE' | 'NOT RLIKE' | 'BETWEEN' | 'IS NULL' | 'IS NOT NULL' export type TableFilterOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | 'IN' | 'NOT IN' | 'LIKE' | 'NOT LIKE' | 'RLIKE' | 'NOT RLIKE' | 'BETWEEN' | 'IS NULL' | 'IS NOT NULL'
@@ -35,17 +35,16 @@ export interface InsertRowsParams {
uid: string; uid: string;
schema: string; schema: string;
table: string; table: string;
row: {[key: string]: { row: Record<string, {
group: string; group: string;
method: string; method: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any; params: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any; value: any;
length: number; length: number;
}; }>;
};
repeat: number; repeat: number;
fields: {[key: string]: string}; fields: Record<string, string>;
locale: UsableLocale; locale: UsableLocale;
} }

View File

@@ -1,3 +1,5 @@
import { match } from 'ciaplu';
function isJSON (str: string) { function isJSON (str: string) {
try { try {
if (!['{', '['].includes(str.trim()[0])) if (!['{', '['].includes(str.trim()[0]))
@@ -176,17 +178,13 @@ function isMD (str: string) {
} }
export function langDetector (str: string) { export function langDetector (str: string) {
if (!str || !str.trim().length) return match(str)
return 'text'; .when(() => !str || !str.trim().length, () => 'text')
if (isJSON(str)) .when(isJSON, () => 'json')
return 'json'; .when(isHTML, () => 'html')
if (isHTML(str)) .when(isSVG, () => 'svg')
return 'html'; .when(isXML, () => 'xml')
if (isSVG(str)) .when(isMD, () => 'markdown')
return 'svg'; .otherwise(() => 'text')
if (isXML(str)) .return();
return 'xml';
if (isMD(str))
return 'markdown';
return 'text';
} }

View File

@@ -1,45 +1,25 @@
import { match } from 'ciaplu';
export function mimeFromHex (hex: string) { export function mimeFromHex (hex: string) {
switch (hex.substring(0, 4)) { // 2 bytes return match(hex.substring(0, 4)) // 2 bytes
case '424D': .with('424D', () => ({ ext: 'bmp', mime: 'image/bmp' }))
return { ext: 'bmp', mime: 'image/bmp' }; .with('1F8B', () => ({ ext: 'tar.gz', mime: 'application/gzip' }))
case '1F8B': .with('0B77', () => ({ ext: 'ac3', mime: 'audio/vnd.dolby.dd-raw' }))
return { ext: 'tar.gz', mime: 'application/gzip' }; .with('7801', () => ({ ext: 'dmg', mime: 'application/x-apple-diskimage' }))
case '0B77': .with('4D5A', () => ({ ext: 'exe', mime: 'application/x-msdownload' }))
return { ext: 'ac3', mime: 'audio/vnd.dolby.dd-raw' }; .when((val) => ['1FA0', '1F9D'].includes(val), () => ({ ext: 'Z', mime: 'application/x-compress' }))
case '7801': .extracting(() => hex.substring(0, 6)) // 3 bytes
return { ext: 'dmg', mime: 'application/x-apple-diskimage' }; .with('FFD8FF', () => ({ ext: 'jpg', mime: 'image/jpeg' }))
case '4D5A': .with('4949BC', () => ({ ext: 'jxr', mime: 'image/vnd.ms-photo' }))
return { ext: 'exe', mime: 'application/x-msdownload' }; .with('425A68', () => ({ ext: 'bz2', mime: 'application/x-bzip2' }))
case '1FA0': .extracting(() => hex) // 4 bytes
case '1F9D': .with('89504E47', () => ({ ext: 'png', mime: 'image/png' }))
return { ext: 'Z', mime: 'application/x-compress' }; .with('47494638', () => ({ ext: 'gif', mime: 'image/gif' }))
default: .with('25504446', () => ({ ext: 'pdf', mime: 'application/pdf' }))
switch (hex.substring(0, 6)) { // 3 bytes .with('504B0304', () => ({ ext: 'zip', mime: 'application/zip' }))
case 'FFD8FF': .with('425047FB', () => ({ ext: 'bpg', mime: 'image/bpg' }))
return { ext: 'jpg', mime: 'image/jpeg' }; .with('4D4D002A', () => ({ ext: 'tif', mime: 'image/tiff' }))
case '4949BC': .with('00000100', () => ({ ext: 'ico', mime: 'image/x-icon' }))
return { ext: 'jxr', mime: 'image/vnd.ms-photo' }; .otherwise(() => ({ ext: '', mime: 'unknown ' + hex }))
case '425A68': .return();
return { ext: 'bz2', mime: 'application/x-bzip2' };
default:
switch (hex) { // 4 bites
case '89504E47':
return { ext: 'png', mime: 'image/png' };
case '47494638':
return { ext: 'gif', mime: 'image/gif' };
case '25504446':
return { ext: 'pdf', mime: 'application/pdf' };
case '504B0304':
return { ext: 'zip', mime: 'application/zip' };
case '425047FB':
return { ext: 'bpg', mime: 'image/bpg' };
case '4D4D002A':
return { ext: 'tif', mime: 'image/tiff' };
case '00000100':
return { ext: 'ico', mime: 'image/x-icon' };
default:
return { ext: '', mime: 'unknown ' + hex };
}
}
}
} }

View File

@@ -0,0 +1,86 @@
import { ClientCode } from 'common/interfaces/antares';
export const querySplitter =(sql: string, dbType: ClientCode): string[] => {
const queries: string[] = [];
let currentQuery = '';
let insideBlock = false;
let insideString = false;
let stringDelimiter: string | null = null;
let insideDollarTag = false;
let dollarTagDelimiter: string | null = null;
// Regex patterns for BEGIN-END blocks, dollar tags in PostgreSQL, and semicolons
const beginRegex = /\bBEGIN\b/i;
const endRegex = /\bEND\b;/i;
const dollarTagRegex = /\$(\w+)?\$/; // Matches $tag$ or $$
// Split on semicolons, keeping semicolons attached to the lines
const lines = sql.split(/(?<=;)/);
for (let line of lines) {
line = line.trim();
if (!line) continue;
for (let i = 0; i < line.length; i++) {
const char = line[i];
// Handle string boundaries
if ((char === '\'' || char === '"') && (!insideString || char === stringDelimiter)) {
if (!insideString) {
insideString = true;
stringDelimiter = char;
}
else {
insideString = false;
stringDelimiter = null;
}
}
currentQuery += char;
if (dbType === 'pg') {
// Handle dollar-quoted blocks in PostgreSQL
if (!insideString && line.slice(i).match(dollarTagRegex)) {
const match = line.slice(i).match(dollarTagRegex);
if (match) {
const tag = match[0];
if (!insideDollarTag) {
insideDollarTag = true;
dollarTagDelimiter = tag;
currentQuery += tag;
i += tag.length - 1;
}
else if (dollarTagDelimiter === tag) {
insideDollarTag = false;
dollarTagDelimiter = null;
currentQuery += tag;
i += tag.length - 1;
}
}
}
}
// Check BEGIN-END blocks
if (!insideString && !insideDollarTag) {
if (beginRegex.test(line))
insideBlock = true;
if (insideBlock && endRegex.test(line))
insideBlock = false;
}
}
// Append the query if we encounter a semicolon outside a BEGIN-END block, outside a string, and outside dollar tags
if (!insideBlock && !insideString && !insideDollarTag && /;\s*$/.test(line)) {
queries.push(currentQuery.trim());
currentQuery = '';
}
}
// Add any remaining query
if (currentQuery.trim())
queries.push(currentQuery.trim());
return queries;
};

View File

@@ -2,6 +2,7 @@
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
import { lineString, point, polygon } from '@turf/helpers'; import { lineString, point, polygon } from '@turf/helpers';
import { BIT, BLOB, DATE, DATETIME, FLOAT, IS_MULTI_SPATIAL, NUMBER, SPATIAL, TEXT_SEARCH } from 'common/fieldTypes'; import { BIT, BLOB, DATE, DATETIME, FLOAT, IS_MULTI_SPATIAL, NUMBER, SPATIAL, TEXT_SEARCH } from 'common/fieldTypes';
import * as antares from 'common/interfaces/antares';
import * as moment from 'moment'; import * as moment from 'moment';
import customizations from '../customizations'; import customizations from '../customizations';
@@ -40,18 +41,21 @@ export const objectToGeoJSON = (val: any) => {
export const escapeAndQuote = (val: string, client: ClientCode) => { export const escapeAndQuote = (val: string, client: ClientCode) => {
const { stringsWrapper: sw } = customizations[client]; const { stringsWrapper: sw } = customizations[client];
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; const CHARS_TO_ESCAPE = sw === '"' ? /[\0\b\t\n\r\x1a"'\\]/g : /[\0\b\t\n\r\x1a'\\]/g;
const CHARS_ESCAPE_MAP: {[key: string]: string} = { const CHARS_ESCAPE_MAP: Record<string, string> = {
'\0': '\\0', '\0': '\\0',
'\b': '\\b', '\b': '\\b',
'\t': '\\t', '\t': '\\t',
'\n': '\\n', '\n': '\\n',
'\r': '\\r', '\r': '\\r',
'\x1a': '\\Z', '\x1a': '\\Z',
'"': '\\"',
'\'': '\\\'', '\'': '\\\'',
'\\': '\\\\' '\\': '\\\\'
}; };
if (sw === '"')
CHARS_ESCAPE_MAP['"'] = '\\"';
let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0; let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0;
let escapedVal = ''; let escapedVal = '';
let match; let match;
@@ -97,10 +101,19 @@ export const valueToSqlString = (args: {
} }
else if ('isArray' in field && field.isArray) { else if ('isArray' in field && field.isArray) {
let localVal; let localVal;
if (Array.isArray(val)) if (Array.isArray(val)) {
localVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}'); localVal = JSON
else .stringify(val)
localVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : ''; .replaceAll('[', '{')
.replaceAll(']', '}');
}
else {
localVal = typeof val === 'string'
? val
.replaceAll('[', '{')
.replaceAll(']', '}')
: '';
}
parsedValue = `'${localVal}'`; parsedValue = `'${localVal}'`;
} }
else if (TEXT_SEARCH.includes(field.type)) else if (TEXT_SEARCH.includes(field.type))
@@ -153,9 +166,9 @@ export const valueToSqlString = (args: {
}; };
export const jsonToSqlInsert = (args: { export const jsonToSqlInsert = (args: {
json: { [key: string]: any}[]; json: Record<string, any>[];
client: ClientCode; client: ClientCode;
fields: { [key: string]: {type: string; datePrecision: number}}; fields: Record<string, {type: string; datePrecision: number}>;
table: string; table: string;
options?: {sqlInsertAfter: number; sqlInsertDivider: 'bytes' | 'rows'}; options?: {sqlInsertAfter: number; sqlInsertDivider: 'bytes' | 'rows'};
}) => { }) => {
@@ -163,7 +176,7 @@ export const jsonToSqlInsert = (args: {
const sqlInsertAfter = options && options.sqlInsertAfter ? options.sqlInsertAfter : 1; const sqlInsertAfter = options && options.sqlInsertAfter ? options.sqlInsertAfter : 1;
const sqlInsertDivider = options && options.sqlInsertDivider ? options.sqlInsertDivider : 'rows'; const sqlInsertDivider = options && options.sqlInsertDivider ? options.sqlInsertDivider : 'rows';
const { elementsWrapper: ew } = customizations[client]; const { elementsWrapper: ew } = customizations[client];
const fieldNames = Object.keys(json[0]).map(key => `${ew}${key}${ew}`); const fieldNames = Object.keys(json[0]).map(key => `${ew}${key.split('.').pop()}${ew}`);
let insertStmt = `INSERT INTO ${ew}${table}${ew} (${fieldNames.join(', ')}) VALUES `; let insertStmt = `INSERT INTO ${ew}${table}${ew} (${fieldNames.join(', ')}) VALUES `;
let insertsString = ''; let insertsString = '';
let queryLength = 0; let queryLength = 0;
@@ -197,3 +210,20 @@ export const jsonToSqlInsert = (args: {
return insertsString; return insertsString;
}; };
export const formatJsonForSqlWhere = (jsonValue: object, clientType: antares.ClientCode) => {
const formattedValue = JSON.stringify(jsonValue);
switch (clientType) {
case 'mysql':
return ` = CAST('${formattedValue}' AS JSON)`;
case 'maria':
return ` = '${formattedValue}'`;
case 'pg':
return `::text = '${formattedValue}'`;
case 'firebird':
case 'sqlite':
default:
return ` = '${formattedValue}'`;
}
};

View File

@@ -1,26 +1,34 @@
export const shortcutEvents: { [key: string]: { l18n: string; l18nParam?: string | number; context?: 'tab' }} = { export const shortcutEvents: Record<string, { i18n: string; i18nParam?: string | number; context?: 'tab' | 'main' }> = {
'run-or-reload': { l18n: 'application.runOrReload', context: 'tab' }, 'run-or-reload': { i18n: 'application.runOrReload', context: 'tab' },
'open-new-tab': { l18n: 'application.openNewTab', context: 'tab' }, 'open-new-tab': { i18n: 'application.openNewTab', context: 'tab' },
'close-tab': { l18n: 'application.closeTab', context: 'tab' }, 'close-tab': { i18n: 'application.closeTab', context: 'tab' },
'format-query': { l18n: 'database.formatQuery', context: 'tab' }, 'format-query': { i18n: 'database.formatQuery', context: 'tab' },
'kill-query': { l18n: 'database.killQuery', context: 'tab' }, 'kill-query': { i18n: 'database.killQuery', context: 'tab' },
'query-history': { l18n: 'database.queryHistory', context: 'tab' }, 'query-history': { i18n: 'database.queryHistory', context: 'tab' },
'clear-query': { l18n: 'database.clearQuery', context: 'tab' }, 'clear-query': { i18n: 'database.clearQuery', context: 'tab' },
'next-tab': { l18n: 'application.nextTab' }, // 'save-file': { i18n: 'application.saveFile', context: 'tab' },
'prev-tab': { l18n: 'application.previousTab' }, 'open-file': { i18n: 'application.openFile', context: 'tab' },
'open-all-connections': { l18n: 'application.openAllConnections' }, 'save-file-as': { i18n: 'application.saveFileAs', context: 'tab' },
'open-filter': { l18n: 'application.openFilter' }, 'next-tab': { i18n: 'application.nextTab' },
'next-page': { l18n: 'application.nextResultsPage' }, 'prev-tab': { i18n: 'application.previousTab' },
'prev-page': { l18n: 'application.previousResultsPage' }, 'open-all-connections': { i18n: 'application.openAllConnections' },
'toggle-console': { l18n: 'application.toggleConsole' }, 'open-filter': { i18n: 'application.openFilter' },
'save-content': { l18n: 'application.saveContent' }, 'next-page': { i18n: 'application.nextResultsPage' },
'create-connection': { l18n: 'connection.createNewConnection' }, 'prev-page': { i18n: 'application.previousResultsPage' },
'open-settings': { l18n: 'application.openSettings' }, 'toggle-console': { i18n: 'application.toggleConsole' },
'open-scratchpad': { l18n: 'application.openScratchpad' } 'save-content': { i18n: 'application.saveContent' },
'create-connection': { i18n: 'connection.createNewConnection' },
'open-settings': { i18n: 'application.openSettings' },
'open-scratchpad': { i18n: 'application.openNotes' },
setFullScreen: { i18n: 'application.fullScreen', context: 'main' },
setZoomIn: { i18n: 'application.zoomIn', context: 'main' },
setZoomOut: { i18n: 'application.zoomOut', context: 'main' },
setZoomReset: { i18n: 'application.zoomReset', context: 'main' }
}; };
interface ShortcutRecord { interface ShortcutRecord {
event: string; event: string;
isFunction?: boolean;
keys: Electron.Accelerator[] | string[]; keys: Electron.Accelerator[] | string[];
/** Needed for default shortcuts */ /** Needed for default shortcuts */
os: NodeJS.Platform[]; os: NodeJS.Platform[];
@@ -35,6 +43,30 @@ const shortcuts: ShortcutRecord[] = [
keys: ['F5'], keys: ['F5'],
os: ['darwin', 'linux', 'win32'] os: ['darwin', 'linux', 'win32']
}, },
{
event: 'setFullScreen',
isFunction: true,
keys: ['F11'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'setZoomIn',
isFunction: true,
keys: ['CommandOrControl+='],
os: ['darwin', 'linux', 'win32']
},
{
event: 'setZoomOut',
isFunction: true,
keys: ['CommandOrControl+-'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'setZoomReset',
isFunction: true,
keys: ['CommandOrControl+0'],
os: ['darwin', 'linux', 'win32']
},
{ {
event: 'save-content', event: 'save-content',
keys: ['CommandOrControl+S'], keys: ['CommandOrControl+S'],
@@ -119,13 +151,28 @@ const shortcuts: ShortcutRecord[] = [
event: 'toggle-console', event: 'toggle-console',
keys: ['CommandOrControl+`'], keys: ['CommandOrControl+`'],
os: ['darwin', 'linux', 'win32'] os: ['darwin', 'linux', 'win32']
},
// {
// event: 'save-file',
// keys: ['CommandOrControl+S'],
// os: ['darwin', 'linux', 'win32']
// },
{
event: 'open-file',
keys: ['CommandOrControl+O'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'save-file-as',
keys: ['Shift+CommandOrControl+S'],
os: ['darwin', 'linux', 'win32']
} }
]; ];
for (let i = 1; i <= 9; i++) { for (let i = 1; i <= 9; i++) {
shortcutEvents[`select-tab-${i}`] = { shortcutEvents[`select-tab-${i}`] = {
l18n: 'application.selectTabNumber', i18n: 'application.selectTabNumber',
l18nParam: i i18nParam: i
}; };
shortcuts.push({ shortcuts.push({

View File

@@ -1,5 +1,6 @@
import { app, dialog, ipcMain, safeStorage } from 'electron'; import { app, dialog, ipcMain, safeStorage } from 'electron';
import * as Store from 'electron-store'; import * as Store from 'electron-store';
import * as fs from 'fs';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
import { ShortcutRegister } from '../libs/ShortcutRegister'; import { ShortcutRegister } from '../libs/ShortcutRegister';
@@ -52,6 +53,11 @@ export default () => {
return dialog.showOpenDialog(options); return dialog.showOpenDialog(options);
}); });
ipcMain.handle('show-save-dialog', (event, options) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
return dialog.showSaveDialog(options);
});
ipcMain.handle('get-download-dir-path', (event) => { ipcMain.handle('get-download-dir-path', (event) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
return app.getPath('downloads'); return app.getPath('downloads');
@@ -80,4 +86,26 @@ export default () => {
const shortCutRegister = ShortcutRegister.getInstance(); const shortCutRegister = ShortcutRegister.getInstance();
shortCutRegister.unregister(); shortCutRegister.unregister();
}); });
ipcMain.handle('read-file', (event, { filePath, encoding }) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
const content = fs.readFileSync(filePath, encoding);
return content;
}
catch (error) {
return { status: 'error', response: error.toString() };
}
});
ipcMain.handle('write-file', (event, filePath, content) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
fs.writeFileSync(filePath, content, 'utf-8');
return { status: 'success' };
}
catch (error) {
return { status: 'error', response: error.toString() };
}
});
}; };

View File

@@ -5,17 +5,28 @@ import { SslOptions } from 'mysql2';
import { ClientsFactory } from '../libs/ClientsFactory'; import { ClientsFactory } from '../libs/ClientsFactory';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
const isAborting: Record<string, boolean> = {};
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('test-connection', async (event, conn: antares.ConnectionParams) => { ipcMain.handle('test-connection', async (event, conn: antares.ConnectionParams) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
let isLocalAborted = false;
const abortChecker = setInterval(() => { // Intercepts abort request
if (isAborting[conn.uid]) {
isAborting[conn.uid] = false;
isLocalAborted = true;
clearInterval(abortChecker);
}
}, 50);
const params = { const params = {
host: conn.host, host: conn.host,
port: +conn.port, port: +conn.port,
user: conn.user, user: conn.user,
password: conn.password, password: conn.password,
readonly: conn.readonly, readonly: conn.readonly,
connectionString: conn.connString,
database: '', database: '',
schema: '', schema: '',
databasePath: '', databasePath: '',
@@ -53,9 +64,9 @@ export default (connections: {[key: string]: antares.Client}) => {
username: conn.sshUser, username: conn.sshUser,
password: conn.sshPass, password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22, port: conn.sshPort ? conn.sshPort : 22,
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey).toString() : null, privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey).toString() : undefined,
passphrase: conn.sshPassphrase, passphrase: conn.sshPassphrase,
keepaliveInterval: conn.sshKeepAliveInterval ? conn.sshKeepAliveInterval*1000 : null keepaliveInterval: conn.sshKeepAliveInterval ? conn.sshKeepAliveInterval*1000 : undefined
}; };
} }
@@ -65,19 +76,28 @@ export default (connections: {[key: string]: antares.Client}) => {
client: conn.client, client: conn.client,
params params
}); });
await connection.connect();
if (conn.client === 'firebird') await connection.connect();
connection.raw('SELECT rdb$get_context(\'SYSTEM\', \'DB_NAME\') FROM rdb$database'); if (isLocalAborted) {
else connection.destroy();
await connection.select('1+1').run(); return;
}
await connection.ping();
connection.destroy(); connection.destroy();
clearInterval(abortChecker);
return { status: 'success' }; return { status: 'success' };
} }
catch (err) { catch (error) {
return { status: 'error', response: err.toString() }; clearInterval(abortChecker);
if (error instanceof AggregateError)
throw new Error(error.errors.reduce((acc, curr) => acc +' | '+ curr.message, ''));
else if (!isLocalAborted)
return { status: 'error', response: error.toString() };
else
return { status: 'abort', response: 'Connection aborted' };
} }
}); });
@@ -88,6 +108,15 @@ export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('connect', async (event, conn: antares.ConnectionParams) => { ipcMain.handle('connect', async (event, conn: antares.ConnectionParams) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
let isLocalAborted = false;
const abortChecker = setInterval(() => { // Intercepts abort request
if (isAborting[conn.uid]) {
isAborting[conn.uid] = false;
isLocalAborted = true;
clearInterval(abortChecker);
}
}, 50);
const params = { const params = {
host: conn.host, host: conn.host,
port: +conn.port, port: +conn.port,
@@ -95,6 +124,7 @@ export default (connections: {[key: string]: antares.Client}) => {
password: conn.password, password: conn.password,
application_name: 'Antares SQL', application_name: 'Antares SQL',
readonly: conn.readonly, readonly: conn.readonly,
connectionString: conn.connString,
database: '', database: '',
schema: '', schema: '',
databasePath: '', databasePath: '',
@@ -146,22 +176,40 @@ export default (connections: {[key: string]: antares.Client}) => {
uid: conn.uid, uid: conn.uid,
client: conn.client, client: conn.client,
params, params,
poolSize: 5 poolSize: conn.singleConnectionMode ? 0 : 5
}); });
await connection.connect(); await connection.connect();
if (isLocalAborted) {
connection.destroy();
return { status: 'abort', response: 'Connection aborted' };
}
const structure = await connection.getStructure(new Set()); const structure = await connection.getStructure(new Set());
if (isLocalAborted) {
connection.destroy();
return { status: 'abort', response: 'Connection aborted' };
}
connections[conn.uid] = connection; connections[conn.uid] = connection;
clearInterval(abortChecker);
return { status: 'success', response: structure }; return { status: 'success', response: structure };
} }
catch (err) { catch (err) {
return { status: 'error', response: err.toString() }; clearInterval(abortChecker);
if (!isLocalAborted)
return { status: 'error', response: err.toString() };
else
return { status: 'abort', response: 'Connection aborted' };
} }
}); });
ipcMain.on('abort-connection', (event, uid) => {
isAborting[uid] = true;
});
ipcMain.handle('disconnect', (event, uid) => { ipcMain.handle('disconnect', (event, uid) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-databases', async (event, uid) => { ipcMain.handle('get-databases', async (event, uid) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-function-informations', async (event, params) => { ipcMain.handle('get-function-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -13,7 +13,7 @@ import updates from './updates';
import users from './users'; import users from './users';
import views from './views'; import views from './views';
const connections: {[key: string]: antares.Client} = {}; const connections: Record<string, antares.Client> = {};
export default () => { export default () => {
connection(connections); connection(connections);

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-routine-informations', async (event, params) => { ipcMain.handle('get-routine-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-scheduler-informations', async (event, params) => { ipcMain.handle('get-scheduler-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -6,7 +6,7 @@ import { Worker } from 'worker_threads';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
let exporter: Worker = null; let exporter: Worker = null;
let importer: Worker = null; let importer: Worker = null;
@@ -251,7 +251,7 @@ export default (connections: {[key: string]: antares.Client}) => {
setTimeout(() => { // Ensures that writing thread has finished setTimeout(() => { // Ensures that writing thread has finished
exporter?.terminate(); exporter?.terminate();
exporter = null; exporter = null;
}, 2000); }, 500);
resolve({ status: 'success', response: payload }); resolve({ status: 'success', response: payload });
break; break;
case 'cancel': case 'cancel':

View File

@@ -3,14 +3,14 @@ import { ARRAY, BIT, BLOB, BOOLEAN, DATE, DATETIME, FLOAT, LONG_TEXT, NUMBER, TE
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 { fakerCustom } from 'common/libs/fakerCustom'; import { fakerCustom } from 'common/libs/fakerCustom';
import { sqlEscaper } from 'common/libs/sqlUtils'; import { formatJsonForSqlWhere, sqlEscaper } from 'common/libs/sqlUtils';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as moment from 'moment'; import * as moment from 'moment';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-table-columns', async (event, params) => { ipcMain.handle('get-table-columns', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
@@ -87,6 +87,19 @@ export default (connections: {[key: string]: antares.Client}) => {
} }
}); });
ipcMain.handle('get-table-checks', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
const result = await connections[params.uid].getTableChecks(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-table-ddl', async (event, params) => { ipcMain.handle('get-table-ddl', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
@@ -220,9 +233,10 @@ export default (connections: {[key: string]: antares.Client}) => {
for (const key in orgRow) { for (const key in orgRow) {
if (typeof orgRow[key] === 'string') if (typeof orgRow[key] === 'string')
orgRow[key] = `'${orgRow[key]}'`; orgRow[key] = ` = '${orgRow[key]}'`;
else if (typeof orgRow[key] === 'object' && orgRow[key] !== null)
if (orgRow[key] === null) orgRow[key] = formatJsonForSqlWhere(orgRow[key], connections[params.uid]._client);
else if (orgRow[key] === null)
orgRow[key] = `IS ${orgRow[key]}`; orgRow[key] = `IS ${orgRow[key]}`;
else else
orgRow[key] = `= ${orgRow[key]}`; orgRow[key] = `= ${orgRow[key]}`;
@@ -249,7 +263,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (params.primary) { if (params.primary) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const idString = params.rows.map((row: {[key: string]: any}) => { const idString = params.rows.map((row: Record<string, any>) => {
const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary; const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
return typeof row[fieldName] === 'string' return typeof row[fieldName] === 'string'
@@ -304,10 +318,10 @@ export default (connections: {[key: string]: antares.Client}) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try { // TODO: move to client classes try { // TODO: move to client classes
const rows: {[key: string]: string | number | boolean | Date | Buffer}[] = []; const rows: Record<string, string | number | boolean | Date | Buffer>[] = [];
for (let i = 0; i < +params.repeat; i++) { for (let i = 0; i < +params.repeat; i++) {
const insertObj: {[key: string]: string | number | boolean | Date | Buffer} = {}; const insertObj: Record<string, string | number | boolean | Date | Buffer> = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@@ -367,7 +381,7 @@ export default (connections: {[key: string]: antares.Client}) => {
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
else { // Faker value else { // Faker value
const parsedParams: {[key: string]: string | number | boolean | Date | Buffer} = {}; const parsedParams: Record<string, string | number | boolean | Date | Buffer> = {};
let fakeValue; let fakeValue;
if (params.locale) if (params.locale)
@@ -437,12 +451,12 @@ export default (connections: {[key: string]: antares.Client}) => {
if (description) if (description)
query.select(`LEFT(${description}, 20) AS foreign_description`); query.select(`LEFT(${description}, 20) AS foreign_description`);
const results = await query.run<{[key: string]: string}>(); const results = await query.run<Record<string, string>>();
const parsedResults: {[key: string]: string}[] = []; const parsedResults: Record<string, string>[] = [];
for (const row of results.rows) { for (const row of results.rows) {
const remappedRow: {[key: string]: string} = {}; const remappedRow: Record<string, string> = {};
for (const key in row) for (const key in row)
remappedRow[key.toLowerCase()] = row[key];// Thanks Firebird -.- remappedRow[key.toLowerCase()] = row[key];// Thanks Firebird -.-

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-trigger-informations', async (event, params) => { ipcMain.handle('get-trigger-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-users', async (event, uid) => { ipcMain.handle('get-users', async (event, uid) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };

View File

@@ -3,7 +3,7 @@ import { ipcMain } from 'electron';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: Record<string, antares.Client>) => {
ipcMain.handle('get-view-informations', async (event, params) => { ipcMain.handle('get-view-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
@@ -51,4 +51,52 @@ export default (connections: {[key: string]: antares.Client}) => {
return { status: 'error', response: err.toString() }; return { status: 'error', response: err.toString() };
} }
}); });
ipcMain.handle('get-materialized-view-informations', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
const result = await connections[params.uid].getMaterializedViewInformations(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('drop-materialized-view', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
await connections[params.uid].dropMaterializedView(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('alter-materialized-view', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
await connections[params.uid].alterView(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('create-materialized-view', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
await connections[params.uid].createMaterializedView(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
}; };

View File

@@ -70,23 +70,29 @@ export class ShortcutRegister {
} }
private setLocalShortcuts () { private setLocalShortcuts () {
const isMenuVisible = process.platform === 'darwin';
const submenu = [];
for (const shortcut of this.shortcuts) { for (const shortcut of this.shortcuts) {
if (shortcut.os.includes(process.platform)) { if (shortcut.os.includes(process.platform)) {
for (const key of shortcut.keys) { for (const key of shortcut.keys) {
try { try {
this._menu.append(new MenuItem({ submenu.push({
label: '.', label: String(shortcut.event),
visible: false, accelerator: key,
submenu: [{ visible: isMenuVisible,
label: String(key), click: () => {
accelerator: key, if (shortcut.isFunction) {
visible: false, if (shortcut.event in this) {
click: () => { type exporterMethods = 'setFullScreen' | 'setZoomIn' | 'setZoomOut' | 'setZoomReset';
this._mainWindow.webContents.send(shortcut.event); this[shortcut.event as exporterMethods]();
if (isDevelopment) console.log('LOCAL EVENT:', shortcut); }
} }
}] else
})); this._mainWindow.webContents.send(shortcut.event);
if (isDevelopment) console.log('LOCAL EVENT:', shortcut);
}
});
} }
catch (error) { catch (error) {
if (isDevelopment) console.log(error); if (isDevelopment) console.log(error);
@@ -96,6 +102,11 @@ export class ShortcutRegister {
} }
} }
} }
this._menu.append(new MenuItem({
label: 'Shortcut',
visible: isMenuVisible,
submenu
}));
} }
private setGlobalShortcuts () { private setGlobalShortcuts () {
@@ -118,6 +129,24 @@ export class ShortcutRegister {
} }
} }
setFullScreen () {
this._mainWindow.setFullScreen(!this._mainWindow.isFullScreen());
}
setZoomIn () {
const currentZoom = this._mainWindow.webContents.getZoomLevel();
this._mainWindow.webContents.setZoomLevel(currentZoom + 1);
}
setZoomOut () {
const currentZoom = this._mainWindow.webContents.getZoomLevel();
this._mainWindow.webContents.setZoomLevel(currentZoom - 1);
}
setZoomReset () {
this._mainWindow.webContents.setZoomLevel(0);
}
reload () { reload () {
this.unregister(); this.unregister();
this.init(); this.init();

View File

@@ -2,16 +2,9 @@ import * as antares from 'common/interfaces/antares';
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
import * as pg from 'pg'; import * as pg from 'pg';
import SSH2Promise = require('@fabio286/ssh2-promise'); import SSH2Promise = require('@fabio286/ssh2-promise');
import { querySplitter } from 'common/libs/querySplitter';
const queryLogger = ({ sql, cUid }: {sql: string; cUid: string}) => { import { ipcLogger, LoggerLevel } from '../misc/ipcLogger';
// Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
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' && process.type === 'browser') console.log(escapedSql);
};
/** /**
* As Simple As Possible Query Builder Core * As Simple As Possible Query Builder Core
@@ -22,7 +15,8 @@ export abstract class BaseClient {
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: (args: {sql: string; cUid: string}) => void; protected _logger: (args: {content: string; cUid: string; level: LoggerLevel}) => void;
protected _querySplitter: (sql: string, client: antares.ClientCode) => string[];
protected _queryDefaults: antares.QueryBuilderObject; protected _queryDefaults: antares.QueryBuilderObject;
protected _query: antares.QueryBuilderObject; protected _query: antares.QueryBuilderObject;
@@ -31,7 +25,8 @@ export abstract class BaseClient {
this._cUid = args.uid; 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 || ipcLogger;
this._querySplitter = args.querySplitter || querySplitter;
this._queryDefaults = { this._queryDefaults = {
schema: '', schema: '',
@@ -136,7 +131,7 @@ export abstract class BaseClient {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
insert (arr: {[key: string]: any}[]) { insert (arr: Record<string, any>[]) {
this._query.insert = [...this._query.insert, ...arr]; this._query.insert = [...this._query.insert, ...arr];
return this; return this;
} }
@@ -178,6 +173,10 @@ export abstract class BaseClient {
throw new Error('Method "dropSchema" not implemented'); throw new Error('Method "dropSchema" not implemented');
} }
getTableChecks (...args: any) {
throw new Error('Method "getTableDll" not implemented');
}
getTableDll (...args: any) { getTableDll (...args: any) {
throw new Error('Method "getTableDll" not implemented'); throw new Error('Method "getTableDll" not implemented');
} }
@@ -234,6 +233,18 @@ export abstract class BaseClient {
throw new Error('Method "getVariables" not implemented'); throw new Error('Method "getVariables" not implemented');
} }
getMaterializedViewInformations (...args: any) {
throw new Error('Method "getMaterializedViewInformations" not implemented');
}
dropMaterializedView (...args: any) {
throw new Error('Method "dropMaterializedView" not implemented');
}
createMaterializedView (...args: any) {
throw new Error('Method "createMaterializedView" not implemented');
}
getEventInformations (...args: any) { getEventInformations (...args: any) {
throw new Error('Method "getEventInformations" not implemented'); throw new Error('Method "getEventInformations" not implemented');
} }

View File

@@ -13,7 +13,7 @@ export class FirebirdSQLClient extends BaseClient {
protected _connection?: firebird.Database | firebird.ConnectionPool; protected _connection?: firebird.Database | firebird.ConnectionPool;
_params: firebird.Options; _params: firebird.Options;
private _types: {[key: number]: string} ={ private _types: Record<number, string> ={
452: 'CHAR', // Array of char 452: 'CHAR', // Array of char
448: 'VARCHAR', 448: 'VARCHAR',
500: 'SMALLINT', 500: 'SMALLINT',
@@ -109,6 +109,10 @@ export class FirebirdSQLClient extends BaseClient {
return firebird.pool(this._poolSize, { ...this._params, blobAsText: true }); return firebird.pool(this._poolSize, { ...this._params, blobAsText: true });
} }
ping () {
return this.raw('SELECT rdb$get_context(\'SYSTEM\', \'DB_NAME\') FROM rdb$database');
}
destroy () { destroy () {
if (this._poolSize) if (this._poolSize)
return (this._connection as firebird.ConnectionPool).destroy(); return (this._connection as firebird.ConnectionPool).destroy();
@@ -241,10 +245,10 @@ export class FirebirdSQLClient extends BaseClient {
name: db.name, name: db.name,
size: schemaSize, size: schemaSize,
tables: remappedTables, tables: remappedTables,
functions: [], functions: [] as null[],
procedures: remappedProcedures, procedures: remappedProcedures,
triggers: remappedTriggers, triggers: remappedTriggers,
schedulers: [] schedulers: [] as null[]
}; };
}); });
} }
@@ -333,7 +337,7 @@ export class FirebirdSQLClient extends BaseClient {
return { return {
name: field.FIELD_NAME.trim(), name: field.FIELD_NAME.trim(),
key: null, key: null as null,
type: fieldType, type: fieldType,
schema: schema, schema: schema,
table: table, table: table,
@@ -342,14 +346,14 @@ export class FirebirdSQLClient extends BaseClient {
datePrecision: field.FIELD_NAME.trim() === 'TIMESTAMP' ? 4 : null, datePrecision: field.FIELD_NAME.trim() === 'TIMESTAMP' ? 4 : null,
charLength: ![...NUMBER, ...FLOAT].includes(fieldType) ? field.FIELD_LENGTH : null, charLength: ![...NUMBER, ...FLOAT].includes(fieldType) ? field.FIELD_LENGTH : null,
nullable: !field.NOT_NULL, nullable: !field.NOT_NULL,
unsigned: null, unsigned: null as null,
zerofill: null, zerofill: null as null,
order: field.FIELD_POSITION+1, order: field.FIELD_POSITION+1,
default: defaultValue, default: defaultValue,
charset: field.CHARSET, charset: field.CHARSET,
collation: null, collation: null as null,
autoIncrement: false, autoIncrement: false,
onUpdate: null, onUpdate: null as null,
comment: field.DESCRIPTION?.trim() comment: field.DESCRIPTION?.trim()
}; };
}); });
@@ -453,7 +457,7 @@ export class FirebirdSQLClient extends BaseClient {
table: table, table: table,
field: field.FKCOLUMN_NAME.trim(), field: field.FKCOLUMN_NAME.trim(),
position: field.KEY_SEQ, position: field.KEY_SEQ,
constraintPosition: null, constraintPosition: null as null,
constraintName: field.FK_NAME.trim(), constraintName: field.FK_NAME.trim(),
refSchema: schema, refSchema: schema,
refTable: field.PKTABLE_NAME.trim(), refTable: field.PKTABLE_NAME.trim(),
@@ -1020,7 +1024,7 @@ export class FirebirdSQLClient extends BaseClient {
alias: string; alias: string;
} }
this._logger({ cUid: this._cUid, sql }); this._logger({ cUid: this._cUid, content: sql, level: 'query' });
args = { args = {
nest: false, nest: false,
@@ -1037,9 +1041,7 @@ export class FirebirdSQLClient extends BaseClient {
const resultsArr = []; const resultsArr = [];
let paramsArr = []; let paramsArr = [];
const queries = args.split const queries = args.split
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) ? this._querySplitter(sql, 'firebird')
.filter(Boolean)
.map(q => q.trim())
: [sql]; : [sql];
let connection: firebird.Database | firebird.Transaction; let connection: firebird.Database | firebird.Transaction;

View File

@@ -4,7 +4,9 @@ import dataTypes from 'common/data-types/mysql';
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 * as EncodingToCharset from '../../../../node_modules/mysql2/lib/constants/encoding_charset.js';
import { BaseClient } from './BaseClient'; import { BaseClient } from './BaseClient';
EncodingToCharset.utf8mb3 = 192; // To fix https://github.com/sidorares/node-mysql2/issues/1398 until not included in mysql2
export class MySQLClient extends BaseClient { export class MySQLClient extends BaseClient {
private _schema?: string; private _schema?: string;
@@ -12,10 +14,11 @@ export class MySQLClient extends BaseClient {
private _connectionsToCommit: Map<string, mysql.Connection | mysql.PoolConnection>; private _connectionsToCommit: Map<string, mysql.Connection | mysql.PoolConnection>;
private _keepaliveTimer: NodeJS.Timer; private _keepaliveTimer: NodeJS.Timer;
private _keepaliveMs: number; private _keepaliveMs: number;
private sqlMode?: string[];
_connection?: mysql.Connection | mysql.Pool; _connection?: mysql.Connection | mysql.Pool;
_params: mysql.ConnectionOptions & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean}; _params: mysql.ConnectionOptions & {schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean};
private types: {[key: number]: string} = { private types: Record<number, string> = {
0: 'DECIMAL', 0: 'DECIMAL',
1: 'TINYINT', 1: 'TINYINT',
2: 'SMALLINT', 2: 'SMALLINT',
@@ -58,6 +61,10 @@ export class MySQLClient extends BaseClient {
this._keepaliveMs = 10*60*1000; this._keepaliveMs = 10*60*1000;
} }
private get isPool () {
return 'getConnection' in this._connection;
}
private _getType (field: mysql.FieldPacket & { columnType?: number; columnLength?: number }) { private _getType (field: mysql.FieldPacket & { columnType?: number; columnLength?: number }) {
let name = this.types[field.columnType]; let name = this.types[field.columnType];
let length = field.columnLength; let length = field.columnLength;
@@ -156,6 +163,8 @@ export class MySQLClient extends BaseClient {
this._ssh = new SSH2Promise({ this._ssh = new SSH2Promise({
...this._params.ssh, ...this._params.ssh,
reconnect: true,
reconnectTries: 3,
debug: process.env.NODE_ENV !== 'production' ? (s) => console.log(s) : null debug: process.env.NODE_ENV !== 'production' ? (s) => console.log(s) : null
}); });
@@ -164,13 +173,13 @@ export class MySQLClient extends BaseClient {
remotePort: this._params.port remotePort: this._params.port
}); });
dbConfig.host = (this._ssh.config as SSHConfig[] & { host: string }).host; dbConfig.host = undefined;
dbConfig.port = tunnel.localPort; dbConfig.port = tunnel.localPort;
} }
catch (err) { catch (err) {
if (this._ssh) { if (this._ssh) {
this._ssh.close();
this._ssh.closeTunnel(); this._ssh.closeTunnel();
this._ssh.close();
} }
throw err; throw err;
} }
@@ -181,9 +190,36 @@ export class MySQLClient extends BaseClient {
async connect () { async connect () {
if (!this._poolSize) if (!this._poolSize)
this._connection = await this.getConnection(); this._connection = await this.getSingleConnection();
else else
this._connection = await this.getConnectionPool(); this._connection = await this.getConnectionPool();
// ANSI_QUOTES check
const [response] = await this._connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
this.sqlMode = response[0]?.Value?.split(',');
const hasAnsiQuotes = this.sqlMode.includes('ANSI') || this.sqlMode.includes('ANSI_QUOTES');
if (hasAnsiQuotes)
await this._connection.query(`SET SESSION sql_mode = '${this.sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
if (this._params.readonly)
await this._connection.query('SET SESSION TRANSACTION READ ONLY');
if (this._poolSize) {
const hasAnsiQuotes = this.sqlMode.includes('ANSI') || this.sqlMode.includes('ANSI_QUOTES');
this._connection.on('connection', conn => {
if (this._params.readonly)
conn.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
conn.query(`SET SESSION sql_mode = '${this.sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
});
}
}
ping () {
return this.select('1+1').run();
} }
destroy () { destroy () {
@@ -191,34 +227,24 @@ export class MySQLClient extends BaseClient {
clearInterval(this._keepaliveTimer); clearInterval(this._keepaliveTimer);
this._keepaliveTimer = undefined; this._keepaliveTimer = undefined;
if (this._ssh) { if (this._ssh) {
this._ssh.close();
this._ssh.closeTunnel(); this._ssh.closeTunnel();
this._ssh.close();
} }
} }
async getConnection () { async getSingleConnection () {
const dbConfig = await this.getDbConfig(); const dbConfig = await this.getDbConfig();
const connection = await mysql.createConnection({ const connection = await mysql.createConnection({
...dbConfig, ...dbConfig,
typeCast: (field, next) => { dateStrings: true
if (field.type === 'DATETIME') // typeCast: (field, next) => {
return field.string(); // if (field.type === 'DATETIME')
else // return field.string();
return next(); // else
} // return next();
// }
}); });
// ANSI_QUOTES check
const [response] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode: string[] = response[0]?.Value?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI') || sqlMode.includes('ANSI_QUOTES');
if (this._params.readonly)
await connection.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = '${sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
return connection; return connection;
} }
@@ -227,31 +253,14 @@ export class MySQLClient extends BaseClient {
const connection = mysql.createPool({ const connection = mysql.createPool({
...dbConfig, ...dbConfig,
connectionLimit: this._poolSize, connectionLimit: this._poolSize,
typeCast: (field, next) => { enableKeepAlive: true,
if (field.type === 'DATETIME') dateStrings: true
return field.string(); // typeCast: (field, next) => {
else // if (field.type === 'DATETIME')
return next(); // return field.string();
} // else
}); // return next();
// }
// ANSI_QUOTES check
const [res] = await connection.query<mysql.RowDataPacket[]>('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\'');
const sqlMode: string[] = res[0]?.Value?.split(',');
const hasAnsiQuotes = sqlMode.includes('ANSI') || sqlMode.includes('ANSI_QUOTES');
if (hasAnsiQuotes)
await connection.query(`SET SESSION sql_mode = '${sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
if (this._params.readonly)
await connection.query('SET SESSION TRANSACTION READ ONLY');
connection.on('connection', conn => {
if (this._params.readonly)
conn.query('SET SESSION TRANSACTION READ ONLY');
if (hasAnsiQuotes)
conn.query(`SET SESSION sql_mode = '${sqlMode.filter((m: string) => !['ANSI', 'ANSI_QUOTES'].includes(m)).join(',')}'`);
}); });
this._keepaliveTimer = setInterval(async () => { this._keepaliveTimer = setInterval(async () => {
@@ -261,6 +270,45 @@ export class MySQLClient extends BaseClient {
return connection; return connection;
} }
async getConnection (args?: antares.QueryParams, retry?: boolean): Promise<mysql.Pool | mysql.PoolConnection | mysql.Connection> {
let connection;
try {
if (args && !args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = await this.getSingleConnection();
await connection.query('SET SESSION autocommit=0');
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = this.isPool ? await (this._connection as mysql.Pool).getConnection() : this._connection;
if (args && args.tabUid && this.isPool) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._runningConnections.set(args.tabUid, (connection as any).connection.connectionId);
}
if (args && args.schema)
await connection.query(`USE \`${args.schema}\``);
return connection;
}
catch (error) {
if (error.code === 'ECONNRESET' && !retry) {
this.destroy();
await this.connect();
return this.getConnection(args, true);
}
else if (error instanceof AggregateError)
throw new Error(error.errors.reduce((acc, curr) => acc +' | '+ curr.message, ''));
else
throw new Error(error.message);
}
}
private async keepAlive () { private async keepAlive () {
try { try {
const connection = await (this._connection as mysql.Pool).getConnection(); const connection = await (this._connection as mysql.Pool).getConnection();
@@ -312,10 +360,21 @@ export class MySQLClient extends BaseClient {
if (this._params.schema) if (this._params.schema)
filteredDatabases = filteredDatabases.filter(db => db.Database === this._params.schema); filteredDatabases = filteredDatabases.filter(db => db.Database === this._params.schema);
const { rows: functions } = await this.raw('SHOW FUNCTION STATUS'); /* eslint-disable @typescript-eslint/no-explicit-any */
const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS'); let functions: any[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any let procedures: any[] = [];
let schedulers: any[] = []; let schedulers: any[] = [];
/* eslint-enable @typescript-eslint/no-explicit-any */
try {
const { rows: functionRows } = await this.raw('SHOW FUNCTION STATUS');
const { rows: procedureRows } = await this.raw('SHOW PROCEDURE STATUS');
functions = functionRows;
procedures = procedureRows;
}
catch (err) {
this._logger({ content: err.sqlMessage, cUid: this._cUid, level: 'error' });
}
try { // Avoid exception with event_scheduler DISABLED with MariaDB 10 try { // Avoid exception with event_scheduler DISABLED with MariaDB 10
const { rows } = await this.raw('SELECT *, EVENT_SCHEMA AS `Db`, EVENT_NAME AS `Name` FROM information_schema.`EVENTS`'); const { rows } = await this.raw('SELECT *, EVENT_SCHEMA AS `Db`, EVENT_NAME AS `Name` FROM information_schema.`EVENTS`');
@@ -582,7 +641,7 @@ export class MySQLClient extends BaseClient {
} }
}) })
.filter(Boolean) .filter(Boolean)
.reduce((acc: {[key: string]: { name: string; type: string; length: string; default: string}}, curr) => { .reduce((acc: Record<string, { name: string; type: string; length: string; default: string}>, curr) => {
acc[curr.name] = curr; acc[curr.name] = curr;
return acc; return acc;
}, {}); }, {});
@@ -591,7 +650,7 @@ export class MySQLClient extends BaseClient {
return rows.map((field) => { return rows.map((field) => {
const numLengthMatch = field.COLUMN_TYPE.match(/int\(([^)]+)\)/); const numLengthMatch = field.COLUMN_TYPE.match(/int\(([^)]+)\)/);
const numLength = numLengthMatch ? +numLengthMatch.pop() : field.NUMERIC_PRECISION || null; const numLength = numLengthMatch ? +numLengthMatch.pop() : field.NUMERIC_PRECISION || null;
const enumValues = /(enum)/.test(field.COLUMN_TYPE) const enumValues = /(enum|set)/.test(field.COLUMN_TYPE)
? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1) ? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1)
: null; : null;
@@ -621,7 +680,7 @@ export class MySQLClient extends BaseClient {
charset: field.CHARACTER_SET_NAME, charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME, collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment'), autoIncrement: field.EXTRA.includes('auto_increment'),
generated: field.EXTRA.toLowerCase().includes('generated'), generated: ['VIRTUAL GENERATED', 'VIRTUAL STORED'].includes(field.EXTRA),
onUpdate: field.EXTRA.toLowerCase().includes('on update') onUpdate: field.EXTRA.toLowerCase().includes('on update')
? field.EXTRA.substr(field.EXTRA.indexOf('on update') + 9, field.EXTRA.length).trim() ? field.EXTRA.substr(field.EXTRA.indexOf('on update') + 9, field.EXTRA.length).trim()
: '', : '',
@@ -636,6 +695,34 @@ export class MySQLClient extends BaseClient {
return rows.length ? rows[0].count : 0; return rows.length ? rows[0].count : 0;
} }
async getTableChecks ({ schema, table }: { schema: string; table: string }): Promise<antares.TableCheck[]> {
const { rows } = await this.raw(`
SELECT
CONSTRAINT_NAME as name,
CHECK_CLAUSE as clausole
FROM information_schema.CHECK_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = "${schema}"
AND CONSTRAINT_NAME IN (
SELECT
CONSTRAINT_NAME
FROM
information_schema.TABLE_CONSTRAINTS
WHERE
TABLE_SCHEMA = "${schema}"
AND TABLE_NAME = "${table}"
AND CONSTRAINT_TYPE = 'CHECK'
)
`);
if (rows.length) {
return rows.map(row => ({
name: row.name,
clause: row.clausole
}));
}
return [];
}
async getTableOptions ({ schema, table }: { schema: string; table: string }) { async getTableOptions ({ schema, table }: { schema: string; table: string }) {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
interface TableOptionsResult { interface TableOptionsResult {
@@ -812,11 +899,13 @@ export class MySQLClient extends BaseClient {
fields, fields,
foreigns, foreigns,
indexes, indexes,
checks,
options options
} = params; } = params;
const newColumns: string[] = []; const newColumns: string[] = [];
const newIndexes: string[] = []; const newIndexes: string[] = [];
const newForeigns: string[] = []; const newForeigns: string[] = [];
const newChecks: string[] = [];
let sql = `CREATE TABLE \`${schema}\`.\`${options.name}\``; let sql = `CREATE TABLE \`${schema}\`.\`${options.name}\``;
@@ -857,7 +946,13 @@ export class MySQLClient extends BaseClient {
newForeigns.push(`CONSTRAINT \`${foreign.constraintName}\` FOREIGN KEY (\`${foreign.field}\`) REFERENCES \`${foreign.refTable}\` (\`${foreign.refField}\`) ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`); newForeigns.push(`CONSTRAINT \`${foreign.constraintName}\` FOREIGN KEY (\`${foreign.field}\`) REFERENCES \`${foreign.refTable}\` (\`${foreign.refField}\`) ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
}); });
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')}) COMMENT='${options.comment}', COLLATE='${options.collation}', ENGINE=${options.engine}`; // ADD TABLE CHECKS
checks.forEach(check => {
if (!check.clause.trim().length) return;
newChecks.push(`${check.name ? `CONSTRAINT \`${check.name}\` ` : ''}CHECK (${check.clause})`);
});
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns, ...newChecks].join(', ')}) COMMENT='${options.comment}', COLLATE='${options.collation}', ENGINE=${options.engine}`;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -871,6 +966,7 @@ export class MySQLClient extends BaseClient {
changes, changes,
indexChanges, indexChanges,
foreignChanges, foreignChanges,
checkChanges,
options options
} = params; } = params;
@@ -878,6 +974,7 @@ export class MySQLClient extends BaseClient {
const alterColumnsAdd: string[] = []; const alterColumnsAdd: string[] = [];
const alterColumnsChange: string[] = []; const alterColumnsChange: string[] = [];
const alterColumnsDrop: string[] = []; const alterColumnsDrop: string[] = [];
const alterQueryes: string[] = [];
// OPTIONS // OPTIONS
if ('comment' in options) alterColumnsChange.push(`COMMENT='${options.comment}'`); if ('comment' in options) alterColumnsChange.push(`COMMENT='${options.comment}'`);
@@ -923,6 +1020,12 @@ export class MySQLClient extends BaseClient {
alterColumnsAdd.push(`ADD CONSTRAINT \`${addition.constraintName}\` FOREIGN KEY (\`${addition.field}\`) REFERENCES \`${addition.refTable}\` (\`${addition.refField}\`) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`); alterColumnsAdd.push(`ADD CONSTRAINT \`${addition.constraintName}\` FOREIGN KEY (\`${addition.field}\`) REFERENCES \`${addition.refTable}\` (\`${addition.refField}\`) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
}); });
// ADD TABLE CHECKS
checkChanges.additions.forEach(addition => {
if (!addition.clause.trim().length) return;
alterColumnsAdd.push(`ADD ${addition.name ? `CONSTRAINT \`${addition.name}\` ` : ''}CHECK (${addition.clause})`);
});
// CHANGE FIELDS // CHANGE FIELDS
changes.forEach(change => { changes.forEach(change => {
const typeInfo = this.getTypeInfo(change.type); const typeInfo = this.getTypeInfo(change.type);
@@ -934,9 +1037,9 @@ export class MySQLClient extends BaseClient {
${change.zerofill ? 'ZEROFILL' : ''} ${change.zerofill ? 'ZEROFILL' : ''}
${change.nullable ? 'NULL' : 'NOT NULL'} ${change.nullable ? 'NULL' : 'NOT NULL'}
${change.autoIncrement ? 'AUTO_INCREMENT' : ''} ${change.autoIncrement ? 'AUTO_INCREMENT' : ''}
${change.collation ? `COLLATE ${change.collation}` : ''}
${change.default !== null ? `DEFAULT ${change.default || '\'\''}` : ''} ${change.default !== null ? `DEFAULT ${change.default || '\'\''}` : ''}
${change.comment ? `COMMENT '${change.comment}'` : ''} ${change.comment ? `COMMENT '${change.comment}'` : ''}
${change.collation ? `COLLATE ${change.collation}` : ''}
${change.onUpdate ? `ON UPDATE ${change.onUpdate}` : ''} ${change.onUpdate ? `ON UPDATE ${change.onUpdate}` : ''}
${change.after ? `AFTER \`${change.after}\`` : 'FIRST'}`); ${change.after ? `AFTER \`${change.after}\`` : 'FIRST'}`);
}); });
@@ -967,6 +1070,13 @@ export class MySQLClient extends BaseClient {
alterColumnsChange.push(`ADD CONSTRAINT \`${change.constraintName}\` FOREIGN KEY (\`${change.field}\`) REFERENCES \`${change.refTable}\` (\`${change.refField}\`) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`); alterColumnsChange.push(`ADD CONSTRAINT \`${change.constraintName}\` FOREIGN KEY (\`${change.field}\`) REFERENCES \`${change.refTable}\` (\`${change.refField}\`) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
}); });
// CHANGE CHECK TABLE
checkChanges.changes.forEach(change => {
if (!change.clause.trim().length) return;
alterQueryes.push(`${sql} DROP CONSTRAINT \`${change.name}\``);
alterQueryes.push(`${sql} ADD ${change.name ? `CONSTRAINT \`${change.name}\` ` : ''}CHECK (${change.clause})`);
});
// DROP FIELDS // DROP FIELDS
deletions.forEach(deletion => { deletions.forEach(deletion => {
alterColumnsDrop.push(`DROP COLUMN \`${deletion.name}\``); alterColumnsDrop.push(`DROP COLUMN \`${deletion.name}\``);
@@ -985,7 +1095,11 @@ export class MySQLClient extends BaseClient {
alterColumnsDrop.push(`DROP FOREIGN KEY \`${deletion.constraintName}\``); alterColumnsDrop.push(`DROP FOREIGN KEY \`${deletion.constraintName}\``);
}); });
const alterQueryes = []; // DROP CHECK TABLE
checkChanges.deletions.forEach(deletion => {
alterQueryes.push(`${sql} DROP CONSTRAINT \`${deletion.name}\``);
});
if (alterColumnsAdd.length) alterQueryes.push(sql+alterColumnsAdd.join(', ')); if (alterColumnsAdd.length) alterQueryes.push(sql+alterColumnsAdd.join(', '));
if (alterColumnsChange.length) alterQueryes.push(sql+alterColumnsChange.join(', ')); if (alterColumnsChange.length) alterQueryes.push(sql+alterColumnsChange.join(', '));
if (alterColumnsDrop.length) alterQueryes.push(sql+alterColumnsDrop.join(', ')); if (alterColumnsDrop.length) alterQueryes.push(sql+alterColumnsDrop.join(', '));
@@ -1625,7 +1739,7 @@ export class MySQLClient extends BaseClient {
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
this._logger({ cUid: this._cUid, sql }); this._logger({ cUid: this._cUid, content: sql, level: 'query' });
args = { args = {
nest: false, nest: false,
@@ -1643,33 +1757,10 @@ export class MySQLClient extends BaseClient {
const resultsArr: antares.QueryResult[] = []; const resultsArr: antares.QueryResult[] = [];
let paramsArr = []; let paramsArr = [];
const queries = args.split const queries = args.split
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) ? this._querySplitter(sql, 'mysql')
.filter(Boolean)
.map(q => q.trim())
: [sql]; : [sql];
let connection: mysql.Connection | mysql.Pool | mysql.PoolConnection; const connection = await this.getConnection(args);
const isPool = 'getConnection' in this._connection;
if (!args.autocommit && args.tabUid) { // autocommit OFF
if (this._connectionsToCommit.has(args.tabUid))
connection = this._connectionsToCommit.get(args.tabUid);
else {
connection = await this.getConnection();
await connection.query('SET SESSION autocommit=0');
this._connectionsToCommit.set(args.tabUid, connection);
}
}
else// autocommit ON
connection = isPool ? await (this._connection as mysql.Pool).getConnection() : this._connection;
if (args.tabUid && isPool) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._runningConnections.set(args.tabUid, (connection as any).connection.connectionId);
}
if (args.schema)
await connection.query(`USE \`${args.schema}\``);
for (const query of queries) { for (const query of queries) {
if (!query) continue; if (!query) continue;
@@ -1682,9 +1773,10 @@ export class MySQLClient extends BaseClient {
connection.query({ sql: query, nestTables }).then(async ([response, fields]) => { connection.query({ sql: query, nestTables }).then(async ([response, fields]) => {
timeStop = new Date(); timeStop = new Date();
const queryResult = response; const queryResult = response;
const fieldsArr = fields ? Array.isArray(fields[0]) ? fields[0] : fields : false;// Some times fields are nested in an array
let remappedFields = fields let remappedFields = fieldsArr
? fields.map(field => { ? fieldsArr.map(field => {
if (!field || Array.isArray(field)) if (!field || Array.isArray(field))
return undefined; return undefined;
@@ -1729,7 +1821,7 @@ export class MySQLClient extends BaseClient {
}); });
} }
catch (err) { catch (err) {
if (isPool && args.autocommit) { if (this.isPool && args.autocommit) {
(connection as mysql.PoolConnection).release(); (connection as mysql.PoolConnection).release();
this._runningConnections.delete(args.tabUid); this._runningConnections.delete(args.tabUid);
} }
@@ -1741,7 +1833,7 @@ export class MySQLClient extends BaseClient {
keysArr = keysArr ? [...keysArr, ...response] : response; keysArr = keysArr ? [...keysArr, ...response] : response;
} }
catch (err) { catch (err) {
if (isPool && args.autocommit) { if (this.isPool && args.autocommit) {
(connection as mysql.PoolConnection).release(); (connection as mysql.PoolConnection).release();
this._runningConnections.delete(args.tabUid); this._runningConnections.delete(args.tabUid);
} }
@@ -1753,13 +1845,13 @@ export class MySQLClient extends BaseClient {
resolve({ resolve({
duration: timeStop.getTime() - timeStart.getTime(), duration: timeStop.getTime() - timeStart.getTime(),
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false, rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? queryResult[0] : queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false, report: !Array.isArray(queryResult) ? queryResult : false,
fields: remappedFields, fields: remappedFields,
keys: keysArr keys: keysArr
}); });
}).catch((err) => { }).catch((err) => {
if (isPool && args.autocommit) { if (this.isPool && args.autocommit) {
(connection as mysql.PoolConnection).release(); (connection as mysql.PoolConnection).release();
this._runningConnections.delete(args.tabUid); this._runningConnections.delete(args.tabUid);
} }
@@ -1776,7 +1868,7 @@ export class MySQLClient extends BaseClient {
}); });
} }
if (isPool && args.autocommit) { if (this.isPool && args.autocommit) {
(connection as mysql.PoolConnection).release(); (connection as mysql.PoolConnection).release();
this._runningConnections.delete(args.tabUid); this._runningConnections.delete(args.tabUid);
} }

View File

@@ -88,8 +88,8 @@ export class PostgreSQLClient extends BaseClient {
private _keepaliveTimer: NodeJS.Timer; private _keepaliveTimer: NodeJS.Timer;
private _keepaliveMs: number; private _keepaliveMs: number;
protected _connection?: pg.Client | pg.Pool; protected _connection?: pg.Client | pg.Pool;
private types: {[key: string]: string} = {}; private types: Record<string, string> = {};
private _arrayTypes: {[key: string]: string} = { private _arrayTypes: Record<string, string> = {
_int2: 'SMALLINT', _int2: 'SMALLINT',
_int4: 'INTEGER', _int4: 'INTEGER',
_int8: 'BIGINT', _int8: 'BIGINT',
@@ -155,6 +155,7 @@ export class PostgreSQLClient extends BaseClient {
host: this._params.host, host: this._params.host,
port: this._params.port, port: this._params.port,
user: this._params.user, user: this._params.user,
connectionString: this._params.connectionString,
database: 'postgres' as string, database: 'postgres' as string,
password: this._params.password, password: this._params.password,
ssl: null as ConnectionOptions ssl: null as ConnectionOptions
@@ -168,6 +169,8 @@ export class PostgreSQLClient extends BaseClient {
try { try {
this._ssh = new SSH2Promise({ this._ssh = new SSH2Promise({
...this._params.ssh, ...this._params.ssh,
reconnect: true,
reconnectTries: 3,
debug: process.env.NODE_ENV !== 'production' ? (s) => console.log(s) : null debug: process.env.NODE_ENV !== 'production' ? (s) => console.log(s) : null
}); });
@@ -176,7 +179,7 @@ export class PostgreSQLClient extends BaseClient {
remotePort: this._params.port remotePort: this._params.port
}); });
dbConfig.host = (this._ssh.config as SSHConfig[] & { host: string }).host; dbConfig.host = undefined;
dbConfig.port = tunnel.localPort; dbConfig.port = tunnel.localPort;
} }
catch (err) { catch (err) {
@@ -210,6 +213,10 @@ export class PostgreSQLClient extends BaseClient {
if (this._params.readonly) if (this._params.readonly)
await connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY'); await connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
connection.on('error', err => { // Intercepts errors and converts to rejections
Promise.reject(err);
});
return connection; return connection;
} }
@@ -239,6 +246,10 @@ export class PostgreSQLClient extends BaseClient {
return connection; return connection;
} }
ping () {
return this.select('1+1').run();
}
destroy () { destroy () {
this._connection.end(); this._connection.end();
clearInterval(this._keepaliveTimer); clearInterval(this._keepaliveTimer);
@@ -331,6 +342,19 @@ export class PostgreSQLClient extends BaseClient {
ORDER BY table_name ORDER BY table_name
`); `);
let { rows: matViews } = await this.raw<antares.QueryResult<ShowTableResult>>(`
SELECT schemaname AS schema_name,
matviewname AS table_name,
matviewowner AS owner,
ispopulated AS is_populated,
definition,
'materializedView' AS table_type
FROM pg_matviews
WHERE schemaname = '${db.database}'
ORDER BY schema_name,
table_name;
`);
if (tables.length) { if (tables.length) {
tables = tables.map(table => { tables = tables.map(table => {
table.Db = db.database; table.Db = db.database;
@@ -339,6 +363,14 @@ export class PostgreSQLClient extends BaseClient {
tablesArr.push(...tables); tablesArr.push(...tables);
} }
if (matViews.length) {
matViews = matViews.map(view => {
view.Db = db.database;
return view;
});
tablesArr.push(...matViews);
}
let { rows: triggers } = await this.raw<antares.QueryResult<ShowTriggersResult>>(` let { rows: triggers } = await this.raw<antares.QueryResult<ShowTriggersResult>>(`
SELECT SELECT
pg_class.relname AS table_name, pg_class.relname AS table_name,
@@ -374,7 +406,11 @@ export class PostgreSQLClient extends BaseClient {
return { return {
name: table.table_name, name: table.table_name,
type: table.table_type === 'VIEW' ? 'view' : 'table', type: table.table_type === 'VIEW'
? 'view'
: table.table_type === 'materializedView'
? 'materializedView'
: 'table',
rows: table.reltuples, rows: table.reltuples,
size: tableSize, size: tableSize,
collation: table.Collation, collation: table.Collation,
@@ -430,7 +466,7 @@ export class PostgreSQLClient extends BaseClient {
procedures: remappedProcedures, procedures: remappedProcedures,
triggers: remappedTriggers, triggers: remappedTriggers,
triggerFunctions: remappedTriggerFunctions, triggerFunctions: remappedTriggerFunctions,
schedulers: [] schedulers: [] as null[]
}; };
} }
else { else {
@@ -465,16 +501,27 @@ export class PostgreSQLClient extends BaseClient {
column_default: string; column_default: string;
character_set_name: string; character_set_name: string;
collation_name: string; collation_name: string;
column_comment: string;
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
const { rows } = await this // Table columns
.select('*') const { rows } = await this.raw<antares.QueryResult<TableColumnsResult>>(`
.schema('information_schema') WITH comments AS (
.from('columns') SELECT attr.attname AS column, des.description AS comment, pgc.relname
.where({ table_schema: `= '${schema}'`, table_name: `= '${table}'` }) FROM pg_attribute AS attr, pg_description AS des, pg_class AS pgc
.orderBy({ ordinal_position: 'ASC' }) WHERE pgc.oid = attr.attrelid
.run<TableColumnsResult>(); AND des.objoid = pgc.oid
AND pg_table_is_visible(pgc.oid)
AND attr.attnum = des.objsubid
)
SELECT cols.*, comments.comment AS column_comment
FROM "information_schema"."columns" AS cols
LEFT JOIN comments ON comments.column = cols.column_name AND comments.relname = cols.table_name
WHERE cols.table_schema = '${schema}'
AND cols.table_name = '${table}'
ORDER BY "ordinal_position" ASC
`);
return rows.map(field => { return rows.map(field => {
let type = field.data_type; let type = field.data_type;
@@ -485,7 +532,7 @@ export class PostgreSQLClient extends BaseClient {
return { return {
name: field.column_name, name: field.column_name,
key: null, key: null as null,
type: type.toUpperCase(), type: type.toUpperCase(),
isArray, isArray,
schema: field.table_schema, schema: field.table_schema,
@@ -495,15 +542,15 @@ export class PostgreSQLClient extends BaseClient {
datePrecision: field.datetime_precision, datePrecision: field.datetime_precision,
charLength: field.character_maximum_length, charLength: field.character_maximum_length,
nullable: field.is_nullable.includes('YES'), nullable: field.is_nullable.includes('YES'),
unsigned: null, unsigned: null as null,
zerofill: null, zerofill: null as null,
order: field.ordinal_position, order: field.ordinal_position,
default: field.column_default, default: field.column_default,
charset: field.character_set_name, charset: field.character_set_name,
collation: field.collation_name, collation: field.collation_name,
autoIncrement: false, autoIncrement: false,
onUpdate: null, onUpdate: null as null,
comment: '' comment: field.column_comment
}; };
}); });
} }
@@ -562,8 +609,8 @@ export class PostgreSQLClient extends BaseClient {
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
if (schema !== 'public') // if (schema !== 'public')
await this.use(schema); await this.use(schema);
const { rows } = await this.raw<antares.QueryResult<ShowIntexesResult>>(`WITH ndx_list AS ( const { rows } = await this.raw<antares.QueryResult<ShowIntexesResult>>(`WITH ndx_list AS (
SELECT pg_index.indexrelid, pg_class.oid SELECT pg_index.indexrelid, pg_class.oid
@@ -607,35 +654,7 @@ export class PostgreSQLClient extends BaseClient {
}, {} as {table: string; schema: string}[]); }, {} as {table: string; schema: string}[]);
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getTableDll ({ schema, table }: { schema: string; table: string }) { async getTableDll ({ schema, table }: { schema: string; table: string }) {
// const { rows } = await this.raw<antares.QueryResult<{'ddl'?: string}>>(`
// SELECT
// 'CREATE TABLE ' || relname || E'\n(\n' ||
// array_to_string(
// array_agg(' ' || column_name || ' ' || type || ' '|| not_null)
// , E',\n'
// ) || E'\n);\n' AS ddl
// FROM (
// SELECT
// a.attname AS column_name
// , pg_catalog.format_type(a.atttypid, a.atttypmod) AS type
// , CASE WHEN a.attnotnull THEN 'NOT NULL' ELSE 'NULL' END AS not_null
// , c.relname
// FROM pg_attribute a, pg_class c, pg_type t
// WHERE a.attnum > 0
// AND a.attrelid = c.oid
// AND a.atttypid = t.oid
// AND c.relname = '${table}'
// ORDER BY a.attnum
// ) AS tabledefinition
// GROUP BY relname
// `);
// if (rows.length)
// return rows[0].ddl;
// else return '';
/* eslint-disable camelcase */ /* eslint-disable camelcase */
interface SequenceRecord { interface SequenceRecord {
sequence_catalog: string; sequence_catalog: string;
@@ -656,7 +675,7 @@ export class PostgreSQLClient extends BaseClient {
let createSql = ''; let createSql = '';
const sequences = []; const sequences = [];
const columnsSql = []; const columnsSql = [];
const arrayTypes: {[key: string]: string} = { const arrayTypes: Record<string, string> = {
_int2: 'smallint', _int2: 'smallint',
_int4: 'integer', _int4: 'integer',
_int8: 'bigint', _int8: 'bigint',
@@ -677,6 +696,34 @@ export class PostgreSQLClient extends BaseClient {
if (!rows.length) return ''; if (!rows.length) return '';
const indexes = await this.getTableIndexes({ schema, table });
const primaryKey = indexes
.filter(i => i.type === 'PRIMARY')
.reduce((acc, cur) => {
if (!Object.keys(acc).length) {
cur.column = `"${cur.column}"`;
acc = cur;
}
else
acc.column += `, "${cur.column}"`;
return acc;
}, {} as { name: string; column: string; type: string});
const remappedIndexes = indexes
.filter(i => i.type !== 'PRIMARY')
.reduce((acc, cur) => {
const existingIndex = acc.findIndex(i => i.name === cur.name);
if (existingIndex >= 0)
acc[existingIndex].column += `, "${cur.column}"`;
else {
cur.column = `"${cur.column}"`;
acc.push(cur);
}
return acc;
}, [] as { name: string; column: string; type: string}[]);
for (const column of rows) { for (const column of rows) {
let fieldType = column.data_type; let fieldType = column.data_type;
if (fieldType === 'USER-DEFINED') fieldType = `"${schema}".${column.udt_name}`; if (fieldType === 'USER-DEFINED') fieldType = `"${schema}".${column.udt_name}`;
@@ -704,6 +751,9 @@ export class PostgreSQLClient extends BaseClient {
columnsSql.push(columnArr.join(' ')); columnsSql.push(columnArr.join(' '));
} }
if (primaryKey)
columnsSql.push(`CONSTRAINT "${primaryKey.name}" PRIMARY KEY (${primaryKey.column})`);
// Table sequences // Table sequences
for (let sequence of sequences) { for (let sequence of sequences) {
if (sequence.includes('.')) sequence = sequence.split('.')[1]; if (sequence.includes('.')) sequence = sequence.split('.')[1];
@@ -720,25 +770,22 @@ export class PostgreSQLClient extends BaseClient {
INCREMENT BY ${rows[0].increment} INCREMENT BY ${rows[0].increment}
MINVALUE ${rows[0].minimum_value} MINVALUE ${rows[0].minimum_value}
MAXVALUE ${rows[0].maximum_value} MAXVALUE ${rows[0].maximum_value}
CACHE 1;\n`; CACHE 1;\n\n`;
} }
} }
// Table create // Table create
createSql += `\nCREATE TABLE "${schema}"."${table}"( createSql += `CREATE TABLE "${schema}"."${table}"(
${columnsSql.join(',\n ')} ${columnsSql.join(',\n ')}
);\n`; );\n`;
// Table indexes // Table indexes
createSql += '\n'; createSql += '\n';
const { rows: indexes } = await this.select('*')
.schema('pg_catalog')
.from('pg_indexes')
.where({ schemaname: `= '${schema}'`, tablename: `= '${table}'` })
.run<{indexdef: string}>();
for (const index of indexes) for (const index of remappedIndexes) {
createSql += `${index.indexdef};\n`; if (index.type !== 'PRIMARY')
createSql += `CREATE ${index.type}${index.type === 'UNIQUE' ? ' INDEX' : ''} "${index.name}" ON "${schema}"."${table}" (${index.column});\n`;
}
return createSql; return createSql;
} }
@@ -840,6 +887,7 @@ export class PostgreSQLClient extends BaseClient {
const newIndexes: string[] = []; const newIndexes: string[] = [];
const manageIndexes: string[] = []; const manageIndexes: string[] = [];
const newForeigns: string[] = []; const newForeigns: string[] = [];
const modifyComment: string[] = [];
let sql = `CREATE TABLE "${schema}"."${options.name}"`; let sql = `CREATE TABLE "${schema}"."${options.name}"`;
@@ -855,6 +903,8 @@ export class PostgreSQLClient extends BaseClient {
${field.nullable ? 'NULL' : 'NOT NULL'} ${field.nullable ? 'NULL' : 'NOT NULL'}
${field.default !== null ? `DEFAULT ${field.default || '\'\''}` : ''} ${field.default !== null ? `DEFAULT ${field.default || '\'\''}` : ''}
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`); ${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
if (field.comment != null)
modifyComment.push(`COMMENT ON COLUMN "${schema}"."${options.name}"."${field.name}" IS '${field.comment}'`);
}); });
// ADD INDEX // ADD INDEX
@@ -875,8 +925,12 @@ export class PostgreSQLClient extends BaseClient {
newForeigns.push(`CONSTRAINT "${foreign.constraintName}" FOREIGN KEY ("${foreign.field}") REFERENCES "${schema}"."${foreign.refTable}" ("${foreign.refField}") ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`); newForeigns.push(`CONSTRAINT "${foreign.constraintName}" FOREIGN KEY ("${foreign.field}") REFERENCES "${schema}"."${foreign.refTable}" ("${foreign.refField}") ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
}); });
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')})`; sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')}); `;
if (manageIndexes.length) sql = `${sql}; ${manageIndexes.join(';')}`; if (manageIndexes.length) sql = `${sql} ${manageIndexes.join(';')}; `;
// TABLE COMMENT
if (options.comment != null) sql = `${sql} COMMENT ON TABLE "${schema}"."${options.name}" IS '${options.comment}'; `;
// FIELDS COMMENT
if (modifyComment.length) sql = `${sql} ${modifyComment.join(';')}; `;
return await this.raw(sql); return await this.raw(sql);
} }
@@ -901,6 +955,7 @@ export class PostgreSQLClient extends BaseClient {
const renameColumns: string[] = []; const renameColumns: string[] = [];
const createSequences: string[] = []; const createSequences: string[] = [];
const manageIndexes: string[] = []; const manageIndexes: string[] = [];
const modifyComment: string[] = [];
// ADD FIELDS // ADD FIELDS
additions.forEach(addition => { additions.forEach(addition => {
@@ -914,6 +969,8 @@ export class PostgreSQLClient extends BaseClient {
${addition.nullable ? 'NULL' : 'NOT NULL'} ${addition.nullable ? 'NULL' : 'NOT NULL'}
${addition.default !== null ? `DEFAULT ${addition.default || '\'\''}` : ''} ${addition.default !== null ? `DEFAULT ${addition.default || '\'\''}` : ''}
${addition.onUpdate ? `ON UPDATE ${addition.onUpdate}` : ''}`); ${addition.onUpdate ? `ON UPDATE ${addition.onUpdate}` : ''}`);
if (addition.comment != null)
modifyComment.push(`COMMENT ON COLUMN "${schema}"."${table}"."${addition.name}" IS '${addition.comment}'`);
}); });
// ADD INDEX // ADD INDEX
@@ -966,6 +1023,8 @@ export class PostgreSQLClient extends BaseClient {
if (change.orgName !== change.name) if (change.orgName !== change.name)
renameColumns.push(`ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${change.orgName}" TO "${change.name}"`); renameColumns.push(`ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${change.orgName}" TO "${change.name}"`);
if (change.comment != null)
modifyComment.push(`COMMENT ON COLUMN "${schema}"."${table}"."${change.name}" IS '${change.comment}'`);
}); });
// CHANGE INDEX // CHANGE INDEX
@@ -1013,8 +1072,11 @@ export class PostgreSQLClient extends BaseClient {
if (alterColumns.length) sql += `ALTER TABLE "${schema}"."${table}" ${alterColumns.join(', ')}; `; if (alterColumns.length) sql += `ALTER TABLE "${schema}"."${table}" ${alterColumns.join(', ')}; `;
if (createSequences.length) sql = `${createSequences.join(';')}; ${sql}`; if (createSequences.length) sql = `${createSequences.join(';')}; ${sql}`;
if (manageIndexes.length) sql = `${manageIndexes.join(';')}; ${sql}`; if (manageIndexes.length) sql = `${manageIndexes.join(';')}; ${sql}`;
// TABLE COMMENT
if (options.comment != null) sql = `${sql} COMMENT ON TABLE "${schema}"."${table}" IS '${options.comment}'; `;
// FIELDS COMMENT
if (modifyComment.length) sql = `${sql} ${modifyComment.join(';')}; `;
if (options.name) sql += `ALTER TABLE "${schema}"."${table}" RENAME TO "${options.name}"; `; if (options.name) sql += `ALTER TABLE "${schema}"."${table}" RENAME TO "${options.name}"; `;
// RENAME // RENAME
if (renameColumns.length) sql = `${renameColumns.join(';')}; ${sql}`; if (renameColumns.length) sql = `${renameColumns.join(';')}; ${sql}`;
@@ -1052,11 +1114,32 @@ export class PostgreSQLClient extends BaseClient {
})[0]; })[0];
} }
async getMaterializedViewInformations ({ schema, view }: { schema: string; view: string }) {
const sql = `SELECT "definition" FROM "pg_matviews" WHERE "matviewname"='${view}' AND "schemaname"='${schema}'`;
const results = await this.raw(sql);
return results.rows.map(row => {
return {
algorithm: '',
definer: '',
security: '',
updateOption: '',
sql: row.definition,
name: view
};
})[0];
}
async dropView (params: { schema: string; view: string }) { async dropView (params: { schema: string; view: string }) {
const sql = `DROP VIEW "${params.schema}"."${params.view}"`; const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql); return await this.raw(sql);
} }
async dropMaterializedView (params: { schema: string; view: string }) {
const sql = `DROP MATERIALIZED VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
async alterView ({ view }: { view: antares.AlterViewParams }) { async alterView ({ view }: { view: antares.AlterViewParams }) {
let sql = `CREATE OR REPLACE VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`; let sql = `CREATE OR REPLACE VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`;
@@ -1066,11 +1149,25 @@ export class PostgreSQLClient extends BaseClient {
return await this.raw(sql); return await this.raw(sql);
} }
async alterMaterializedView ({ view }: { view: antares.AlterViewParams }) {
let sql = `CREATE OR REPLACE MATERIALIZED VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`;
if (view.name !== view.oldName)
sql += `; ALTER VIEW "${view.schema}"."${view.oldName}" RENAME TO "${view.name}"`;
return await this.raw(sql);
}
async createView (params: antares.CreateViewParams) { async createView (params: antares.CreateViewParams) {
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`; const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql); return await this.raw(sql);
} }
async createMaterializedView (params: antares.CreateViewParams) {
const sql = `CREATE MATERIALIZED VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql);
}
async getTriggerInformations ({ schema, trigger }: { schema: string; trigger: string }) { async getTriggerInformations ({ schema, trigger }: { schema: string; trigger: string }) {
const [table, triggerName] = trigger.split('.'); const [table, triggerName] = trigger.split('.');
@@ -1155,9 +1252,9 @@ export class PostgreSQLClient extends BaseClient {
return results.rows.map(async row => { return results.rows.map(async row => {
if (!row.pg_get_functiondef) { if (!row.pg_get_functiondef) {
return { return {
definer: null, definer: null as null,
sql: '', sql: '',
parameters: [], parameters: [] as null[],
name: routine, name: routine,
comment: '', comment: '',
security: 'DEFINER', security: 'DEFINER',
@@ -1206,8 +1303,8 @@ export class PostgreSQLClient extends BaseClient {
name: routine, name: routine,
comment: '', comment: '',
security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER', security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
deterministic: null, deterministic: null as null,
dataAccess: null, dataAccess: null as null,
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0] language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0]
}; };
})[0]; })[0];
@@ -1271,9 +1368,9 @@ export class PostgreSQLClient extends BaseClient {
return results.rows.map(async row => { return results.rows.map(async row => {
if (!row.pg_get_functiondef) { if (!row.pg_get_functiondef) {
return { return {
definer: null, definer: null as null,
sql: '', sql: '',
parameters: [], parameters: [] as null[],
name: func, name: func,
comment: '', comment: '',
security: 'DEFINER', security: 'DEFINER',
@@ -1321,8 +1418,8 @@ export class PostgreSQLClient extends BaseClient {
name: func, name: func,
comment: '', comment: '',
security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER', security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
deterministic: null, deterministic: null as null,
dataAccess: null, dataAccess: null as null,
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0], language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0],
returns: row.pg_get_functiondef.match(/(?<=RETURNS )(.*)(?<=[\S+\n\r\s])/gm)[0].replace('SETOF ', '').toUpperCase() returns: row.pg_get_functiondef.match(/(?<=RETURNS )(.*)(?<=[\S+\n\r\s])/gm)[0].replace('SETOF ', '').toUpperCase()
}; };
@@ -1551,7 +1648,7 @@ export class PostgreSQLClient extends BaseClient {
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
this._logger({ cUid: this._cUid, sql }); this._logger({ cUid: this._cUid, content: sql, level: 'query' });
args = { args = {
nest: false, nest: false,
@@ -1568,9 +1665,7 @@ export class PostgreSQLClient extends BaseClient {
const resultsArr: antares.QueryResult[] = []; const resultsArr: antares.QueryResult[] = [];
let paramsArr = []; let paramsArr = [];
const queries = args.split const queries = args.split
? sql.split(/(?!\B'[^']*);(?![^']*'\B)/gm) ? this._querySplitter(sql, 'pg')
.filter(Boolean)
.map(q => q.trim())
: [sql]; : [sql];
let connection: pg.Client | pg.PoolClient; let connection: pg.Client | pg.PoolClient;

View File

@@ -35,6 +35,10 @@ export class SQLiteClient extends BaseClient {
}); });
} }
ping () {
return this.select('1+1').run();
}
destroy () { destroy () {
this._connection.close(); this._connection.close();
} }
@@ -120,10 +124,10 @@ export class SQLiteClient extends BaseClient {
name: db.name, name: db.name,
size: schemaSize, size: schemaSize,
tables: remappedTables, tables: remappedTables,
functions: [], functions: [] as null[],
procedures: [], procedures: [] as null[],
triggers: remappedTriggers, triggers: remappedTriggers,
schedulers: [] schedulers: [] as null[]
}; };
} }
else { else {
@@ -162,22 +166,22 @@ export class SQLiteClient extends BaseClient {
return { return {
name: field.name, name: field.name,
key: null, key: null as null,
type: type.trim(), type: type.trim(),
schema: schema, schema: schema,
table: table, table: table,
numLength: [...NUMBER, ...FLOAT].includes(type) ? length : null, numLength: [...NUMBER, ...FLOAT].includes(type) ? length : null,
datePrecision: null, datePrecision: null as null,
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null, charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
nullable: !field.notnull, nullable: !field.notnull,
unsigned: null, unsigned: null as null,
zerofill: null, zerofill: null as null,
order: typeof field.cid === 'string' ? +field.cid + 1 : field.cid + 1, order: typeof field.cid === 'string' ? +field.cid + 1 : field.cid + 1,
default: field.dflt_value, default: field.dflt_value,
charset: null, charset: null as null,
collation: null, collation: null as null,
autoIncrement: false, autoIncrement: false,
onUpdate: null, onUpdate: null as null,
comment: '' comment: ''
}; };
}); });
@@ -263,7 +267,7 @@ export class SQLiteClient extends BaseClient {
table: table, table: table,
field: field.from, field: field.from,
position: field.id + 1, position: field.id + 1,
constraintPosition: null, constraintPosition: null as null,
constraintName: field.id, constraintName: field.id,
refSchema: schema, refSchema: schema,
refTable: field.table, refTable: field.table,
@@ -608,7 +612,7 @@ export class SQLiteClient extends BaseClient {
} }
async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) { async raw<T = antares.QueryResult> (sql: string, args?: antares.QueryParams) {
this._logger({ cUid: this._cUid, sql });// TODO: replace BLOB content with a placeholder this._logger({ cUid: this._cUid, content: sql, level: 'query' });// TODO: replace BLOB content with a placeholder
args = { args = {
nest: false, nest: false,
@@ -625,9 +629,7 @@ export class SQLiteClient extends BaseClient {
const resultsArr = []; const resultsArr = [];
let paramsArr = []; let paramsArr = [];
const queries = args.split const queries = args.split
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) ? this._querySplitter(sql, 'sqlite')
.filter(Boolean)
.map(q => q.trim())
: [sql]; : [sql];
let connection: sqlite.Database; let connection: sqlite.Database;
@@ -658,7 +660,7 @@ export class SQLiteClient extends BaseClient {
let queryAllResult: any[]; let queryAllResult: any[];
let affectedRows; let affectedRows;
let fields; let fields;
const detectedTypes: {[key: string]: string} = {}; const detectedTypes: Record<string, string> = {};
try { try {
const stmt = connection.prepare(query); const stmt = connection.prepare(query);

View File

@@ -336,7 +336,8 @@ CREATE TABLE \`${view.Name}\`(
const connection = await this._client.getConnection(); const connection = await this._client.getConnection();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const stream = (connection as any).connection.query(sql).stream(); const stream = (connection as any).connection.query(sql).stream();
const dispose = () => connection.end(); // eslint-disable-next-line @typescript-eslint/no-explicit-any
const dispose = () => (connection as any).release();
stream.on('end', dispose); stream.on('end', dispose);
stream.on('error', dispose); stream.on('error', dispose);
@@ -354,7 +355,7 @@ CREATE TABLE \`${view.Name}\`(
escapeAndQuote (val: string) { escapeAndQuote (val: string) {
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP: {[key: string]: string} = { const CHARS_ESCAPE_MAP: Record<string, string> = {
'\0': '\\0', '\0': '\\0',
'\b': '\\b', '\b': '\\b',
'\t': '\\t', '\t': '\\t',

View File

@@ -39,115 +39,7 @@ SET row_security = off;\n\n\n`;
} }
async getCreateTable (tableName: string) { async getCreateTable (tableName: string) {
/* eslint-disable camelcase */ const createSql = await this._client.getTableDll({ schema: this.schemaName, table: tableName });
interface SequenceRecord {
sequence_catalog: string;
sequence_schema: string;
sequence_name: string;
data_type: string;
numeric_precision: number;
numeric_precision_radix: number;
numeric_scale: number;
start_value: string;
minimum_value: string;
maximum_value: string;
increment: string;
cycle_option: string;
}
/* eslint-enable camelcase */
let createSql = '';
const sequences = [];
const columnsSql = [];
const arrayTypes: {[key: string]: string} = {
_int2: 'smallint',
_int4: 'integer',
_int8: 'bigint',
_float4: 'real',
_float8: 'double precision',
_char: '"char"',
_varchar: 'character varying'
};
// Table columns
const { rows } = await this._client.raw(`
SELECT *
FROM "information_schema"."columns"
WHERE "table_schema" = '${this.schemaName}'
AND "table_name" = '${tableName}'
ORDER BY "ordinal_position" ASC
`, { schema: 'information_schema' });
if (!rows.length) return '';
for (const column of rows) {
let fieldType = column.data_type;
if (fieldType === 'USER-DEFINED') fieldType = `"${this.schemaName}".${column.udt_name}`;
else if (fieldType === 'ARRAY') {
if (Object.keys(arrayTypes).includes(fieldType))
fieldType = arrayTypes[column.udt_name] + '[]';
else
fieldType = column.udt_name.replaceAll('_', '') + '[]';
}
const columnArr = [
`"${column.column_name}"`,
`${fieldType}${column.character_maximum_length ? `(${column.character_maximum_length})` : ''}`
];
if (column.column_default) {
columnArr.push(`DEFAULT ${column.column_default}`);
if (column.column_default.includes('nextval')) {
const sequenceName = column.column_default.split('\'')[1];
sequences.push(sequenceName);
}
}
if (column.is_nullable === 'NO') columnArr.push('NOT NULL');
columnsSql.push(columnArr.join(' '));
}
// Table sequences
for (let sequence of sequences) {
if (sequence.includes('.')) sequence = sequence.split('.')[1];
const { rows } = await this._client
.select('*')
.schema('information_schema')
.from('sequences')
.where({ sequence_schema: `= '${this.schemaName}'`, sequence_name: `= '${sequence}'` })
.run<SequenceRecord>();
if (rows.length) {
createSql += `CREATE SEQUENCE "${this.schemaName}"."${sequence}"
START WITH ${rows[0].start_value}
INCREMENT BY ${rows[0].increment}
MINVALUE ${rows[0].minimum_value}
MAXVALUE ${rows[0].maximum_value}
CACHE 1;\n`;
// createSql += `\nALTER TABLE "${sequence}" OWNER TO ${this._client._params.user};\n\n`;
}
}
// Table create
createSql += `\nCREATE TABLE "${this.schemaName}"."${tableName}"(
${columnsSql.join(',\n ')}
);\n`;
// createSql += `\nALTER TABLE "${tableName}" OWNER TO ${this._client._params.user};\n\n`;
// Table indexes
createSql += '\n';
const { rows: indexes } = await this._client
.select('*')
.schema('pg_catalog')
.from('pg_indexes')
.where({ schemaname: `= '${this.schemaName}'`, tablename: `= '${tableName}'` })
.run<{indexdef: string}>();
for (const index of indexes)
createSql += `${index.indexdef};\n`;
// Table foreigns // Table foreigns
const { rows: foreigns } = await this._client.raw(` const { rows: foreigns } = await this._client.raw(`
@@ -440,7 +332,7 @@ SET row_security = off;\n\n\n`;
escapeAndQuote (val: string) { escapeAndQuote (val: string) {
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g;
const CHARS_ESCAPE_MAP: {[key: string]: string} = { const CHARS_ESCAPE_MAP: Record<string, string> = {
'\0': '\\0', '\0': '\\0',
'\b': '\\b', '\b': '\\b',
'\t': '\\t', '\t': '\\t',

View File

@@ -0,0 +1,20 @@
export type LoggerLevel = 'query' | 'error'
export const ipcLogger = ({ content, cUid, level }: {content: string; cUid: string; level: LoggerLevel}) => {
if (level === 'error') {
if (process.type !== undefined) {
const mainWindow = require('electron').webContents.fromId(1);
mainWindow.send('non-blocking-exception', { cUid, message: content, date: new Date() });
}
if (process.env.NODE_ENV === 'development' && process.type === 'browser') console.log(content);
}
else if (level === 'query') {
// Remove comments, newlines and multiple spaces
const escapedSql = content.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
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' && process.type === 'browser') console.log(escapedSql);
}
};

View File

@@ -43,7 +43,8 @@ async function createMainWindow () {
spellcheck: false spellcheck: false
}, },
autoHideMenuBar: true, autoHideMenuBar: true,
titleBarStyle: isLinux ? 'default' :'hidden', frame: !isLinux,
titleBarStyle: 'hidden',
titleBarOverlay: isWindows titleBarOverlay: isWindows
? { ? {
color: appTheme === 'dark' ? '#3f3f3f' : '#fff', color: appTheme === 'dark' ? '#3f3f3f' : '#fff',
@@ -127,15 +128,25 @@ app.on('ready', async () => {
if (isWindows) if (isWindows)
mainWindow.show(); mainWindow.show();
// if (isDevelopment) // if (isDevelopment && !isWindows)
// mainWindow.webContents.openDevTools(); // mainWindow.webContents.openDevTools();
process.on('uncaughtException', error => { process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error); if (error instanceof AggregateError) {
for (const e of error.errors)
mainWindow.webContents.send('unhandled-exception', e);
}
else
mainWindow.webContents.send('unhandled-exception', error);
}); });
process.on('unhandledRejection', error => { process.on('unhandledRejection', error => {
mainWindow.webContents.send('unhandled-exception', error); if (error instanceof AggregateError) {
for (const e of error.errors)
mainWindow.webContents.send('unhandled-exception', e);
}
else
mainWindow.webContents.send('unhandled-exception', error);
}); });
}); });

View File

@@ -10,9 +10,7 @@
:key="connection.uid" :key="connection.uid"
:connection="connection" :connection="connection"
/> />
<div class="connection-panel-wrapper p-relative"> <WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div>
</div> </div>
<TheFooter /> <TheFooter />
<TheNotificationsBoard /> <TheNotificationsBoard />
@@ -48,6 +46,8 @@ import { useSchemaExportStore } from '@/stores/schemaExport';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { useConsoleStore } from './stores/console';
const { t } = useI18n(); const { t } = useI18n();
const TheTitleBar = defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar.vue')); const TheTitleBar = defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar.vue'));
@@ -80,6 +80,8 @@ const schemaExportStore = useSchemaExportStore();
const { hideExportModal } = schemaExportStore; const { hideExportModal } = schemaExportStore;
const { isExportModal: isExportSchemaModal } = storeToRefs(schemaExportStore); const { isExportModal: isExportSchemaModal } = storeToRefs(schemaExportStore);
const consoleStore = useConsoleStore();
const isAllConnectionsModal: Ref<boolean> = ref(false); const isAllConnectionsModal: Ref<boolean> = ref(false);
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -139,8 +141,11 @@ onMounted(() => {
while (node) { while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) { if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup({ window: getCurrentWindow() }); if (!node.parentNode.className.split(' ').includes('editor-query')) {
break; InputMenu.popup({ window: getCurrentWindow() });
console.log(node.parentNode.className);
break;
}
} }
node = node.parentNode; node = node.parentNode;
} }
@@ -152,6 +157,60 @@ onMounted(() => {
} }
}); });
}); });
// Console messages
const oldLog = console.log;
const oldWarn = console.warn;
const oldInfo = console.info;
const oldError = console.error;
console.log = function (...args) {
consoleStore.putLog('debug', {
level: 'log',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldLog.apply(this, args);
};
console.info = function (...args) {
consoleStore.putLog('debug', {
level: 'info',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldInfo.apply(this, args);
};
console.warn = function (...args) {
consoleStore.putLog('debug', {
level: 'warn',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldWarn.apply(this, args);
};
console.error = function (...args) {
consoleStore.putLog('debug', {
level: 'error',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldError.apply(this, args);
};
window.addEventListener('unhandledrejection', (event) => {
console.error(event.reason);
});
window.addEventListener('error', (event) => {
console.error(event.error, '| File name:', event.filename.split('/').pop().split('?')[0]);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -56,8 +56,8 @@ const { t } = useI18n();
const props = defineProps({ const props = defineProps({
size: { size: {
type: String as PropType<'small' | 'medium' | '400' | 'large'>, type: String as PropType<'small' | 'medium' | '400' | 'large' | 'resize'>,
validator: (prop: string) => ['small', 'medium', '400', 'large'].includes(prop), validator: (prop: string) => ['small', 'medium', '400', 'large', 'resize'].includes(prop),
default: 'small' default: 'small'
}, },
hideFooter: { hideFooter: {
@@ -88,6 +88,8 @@ const modalSizeClass = computed(() => {
return 'modal-sm'; return 'modal-sm';
if (props.size === '400') if (props.size === '400')
return 'modal-400'; return 'modal-400';
if (props.size === 'resize')
return 'modal-resize';
else if (props.size === 'large') else if (props.size === 'large')
return 'modal-lg'; return 'modal-lg';
else return ''; else return '';
@@ -120,6 +122,12 @@ onBeforeUnmount(() => {
max-width: 400px; max-width: 400px;
} }
.modal-resize .modal-container {
max-width: 95vw;
max-height: 95vh;
width: auto;
}
.modal.modal-sm .modal-container { .modal.modal-sm .modal-container {
padding: 0; padding: 0;
} }

View File

@@ -1,11 +1,19 @@
<template> <template>
<SvgIcon <SvgIcon
v-if="type === 'mdi'"
:type="type" :type="type"
:path="iconPath" :path="iconPath"
:size="size" :size="size"
:rotate="rotate" :rotate="rotate"
:class="iconFlip" :class="iconFlip"
/> />
<svg
v-else
:width="size"
:height="size"
:viewBox="`0 0 ${size} ${size}`"
v-html="iconPath"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -13,6 +21,10 @@ import SvgIcon from '@jamescoyle/vue-icon';
import * as Icons from '@mdi/js'; import * as Icons from '@mdi/js';
import { computed, PropType } from 'vue'; import { computed, PropType } from 'vue';
import { useConnectionsStore } from '@/stores/connections';
const { getIconByUid } = useConnectionsStore();
const props = defineProps({ const props = defineProps({
iconName: { iconName: {
type: String, type: String,
@@ -23,21 +35,31 @@ const props = defineProps({
default: 48 default: 48
}, },
type: { type: {
type: String, type: String as PropType<'mdi' | 'custom'>,
default: () => 'mdi' default: () => 'mdi'
}, },
flip: { flip: {
type: String as PropType<'horizontal' | 'vertical' | 'both'>, type: String as PropType<'horizontal' | 'vertical' | 'both' | null>,
default: () => null default: () => null
}, },
rotate: { rotate: {
type: Number, type: Number as PropType<number | null>,
default: () => null default: () => null
} }
}); });
const iconPath = computed(() => { const iconPath = computed(() => {
return (Icons as {[k:string]: string})[props.iconName]; if (props.type === 'mdi')
return (Icons as {[k:string]: string})[props.iconName];
else if (props.type === 'custom') {
const base64 = getIconByUid(props.iconName)?.base64;
const svgString = Buffer
.from(base64, 'base64')
.toString('utf-8');
return svgString;
}
return null;
}); });
const iconFlip = computed(() => { const iconFlip = computed(() => {

View File

@@ -99,7 +99,7 @@ onMounted(() => {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background: $primary-color; background: var(--primary-color);
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 5px 1px darken($body-font-color-dark, 40%); box-shadow: 0 0 5px 1px darken($body-font-color-dark, 40%);
} }

View File

@@ -365,7 +365,11 @@ export default defineComponent({
}; };
const handleWheelEvent = (e) => { const handleWheelEvent = (e) => {
if (!e.target.className.includes('select__')) deactivate(); try {
if (!e.target.className.includes('select__')) deactivate();
}
catch (_) {
}
}; };
onMounted(() => { onMounted(() => {

View File

@@ -4,7 +4,11 @@
:id="`editor-${id}`" :id="`editor-${id}`"
class="editor" class="editor"
:class="editorClass" :class="editorClass"
:style="{height: `${height}px`}" :style="{
height: `${height}px`,
width: width ? `${width}px` : null,
resize: resizable ? 'both' : 'none'
}"
/> />
</div> </div>
</template> </template>
@@ -17,7 +21,7 @@ import 'ace-builds/webpack-resolver';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { onMounted, watch } from 'vue'; import { PropType, onMounted, watch } from 'vue';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
@@ -25,10 +29,12 @@ const props = defineProps({
modelValue: String, modelValue: String,
mode: { type: String, default: 'text' }, mode: { type: String, default: 'text' },
editorClass: { type: String, default: '' }, editorClass: { type: String, default: '' },
resizable: { type: Boolean, default: false },
autoFocus: { type: Boolean, default: false }, autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false }, readOnly: { type: Boolean, default: false },
showLineNumbers: { type: Boolean, default: true }, showLineNumbers: { type: Boolean, default: true },
height: { type: Number, default: 200 } height: { type: Number, default: 200 },
width: { type: [Number, Boolean] as PropType<number|false>, default: false }
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
@@ -132,8 +138,10 @@ onMounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.editor-wrapper { .editor-wrapper {
.editor { .editor {
width: 100%; width: 100%;
} height: 100%;
max-width: 90vw;
}
} }
</style> </style>

View File

@@ -0,0 +1,312 @@
<template>
<div
ref="wrapper"
class="console-wrapper"
@mouseenter="isHover = true"
@mouseleave="isHover = false"
>
<div ref="resizer" class="console-resizer" />
<div
id="console"
ref="queryConsole"
class="console column col-12"
:style="{height: localHeight ? localHeight+'px' : ''}"
>
<div class="console-header">
<ul class="tab tab-block">
<li class="tab-item" :class="{'active': selectedTab === 'query'}">
<a class="tab-link" @click="selectedTab = 'query'">{{ t('application.executedQueries') }}</a>
</li>
<li class="tab-item" :class="{'active': selectedTab === 'debug'}">
<a class="tab-link" @click="selectedTab = 'debug'">{{ t('application.debugConsole') }}</a>
</li>
</ul>
<div class="d-flex">
<div
v-if="isDevelopment"
class="c-hand mr-2"
@click="openDevTools()"
>
<BaseIcon icon-name="mdiBugPlayOutline" :size="22" />
</div>
<div
v-if="isDevelopment"
class="c-hand mr-2"
@click="reload()"
>
<BaseIcon icon-name="mdiRefresh" :size="22" />
</div>
<button class="btn btn-clear mr-1" @click="resizeConsole(0)" />
</div>
</div>
<div
v-show="selectedTab === 'query'"
ref="queryConsoleBody"
class="console-body"
>
<div
v-for="(wLog, i) in workspaceQueryLogs"
:key="i"
class="console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)"
>
<span class="console-log-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="console-log-sql" v-html="highlight(wLog.sql, {html: true})" />
</div>
</div>
<div
v-show="selectedTab === 'debug'"
ref="logConsoleBody"
class="console-body"
>
<div
v-for="(log, i) in debugLogs"
:key="i"
class="console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, log)"
>
<span class="console-log-datetime">{{ moment(log.date).format('HH:mm:ss') }}</span> <small>[{{ log.process.substring(0, 1).toUpperCase() }}]</small>: <span class="console-log-message" :class="`console-log-level-${log.level}`">{{ log.message }}</span>
</div>
</div>
</div>
</div>
<BaseContextMenu
v-if="isContext"
:context-event="contextEvent"
@close-context="isContext = false"
>
<div class="context-element" @click="copyLog">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copy') }}</span>
</div>
</BaseContextMenu>
</template>
<script setup lang="ts">
import { getCurrentWindow } from '@electron/remote';
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { highlight } from 'sql-highlight';
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import { copyText } from '@/libs/copyText';
import { useConsoleStore } from '@/stores/console';
const { t } = useI18n();
const consoleStore = useConsoleStore();
const { resizeConsole, getLogsByWorkspace } = consoleStore;
const {
isConsoleOpen,
consoleHeight,
selectedTab,
debugLogs
} = storeToRefs(consoleStore);
const props = defineProps({
uid: {
type: String,
default: null,
required: false
}
});
const wrapper: Ref<HTMLInputElement> = ref(null);
const queryConsole: Ref<HTMLInputElement> = ref(null);
const queryConsoleBody: Ref<HTMLInputElement> = ref(null);
const logConsoleBody: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null);
const localHeight = ref(consoleHeight.value);
const isHover = ref(false);
const isContext = ref(false);
const contextContent: Ref<string> = ref(null);
const contextEvent: Ref<MouseEvent> = ref(null);
const w = ref(getCurrentWindow());
const isDevelopment = ref(process.env.NODE_ENV === 'development');
const resize = (e: MouseEvent) => {
const el = queryConsole.value;
let elementHeight = el.getBoundingClientRect().bottom - e.pageY;
if (elementHeight > 400) elementHeight = 400;
localHeight.value = elementHeight;
};
const workspaceQueryLogs = computed(() => {
return getLogsByWorkspace(props.uid);
});
const stopResize = () => {
if (localHeight.value < 0) localHeight.value = 0;
resizeConsole(localHeight.value);
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResize);
};
const contextMenu = (event: MouseEvent, wLog: {date: Date; sql?: string; message?: string}) => {
contextEvent.value = event;
contextContent.value = wLog.sql || wLog.message;
isContext.value = true;
};
const copyLog = () => {
copyText(contextContent.value);
isContext.value = false;
};
const openDevTools = () => {
w.value.webContents.openDevTools();
};
const reload = () => {
w.value.reload();
};
watch(workspaceQueryLogs, async () => {
if (!isHover.value) {
await nextTick();
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
}
});
watch(() => debugLogs.value.length, async () => {
if (!isHover.value) {
await nextTick();
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
}
});
watch(isConsoleOpen, async () => {
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
});
watch(selectedTab, async () => {
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
});
watch(consoleHeight, async (val) => {
await nextTick();
localHeight.value = val;
});
onMounted(() => {
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
});
</script>
<style lang="scss" scoped>
.console-wrapper {
width: -webkit-fill-available;
z-index: 9;
margin-top: auto;
position: absolute;
bottom: 0;
.console-resizer {
height: 4px;
top: -1px;
width: 100%;
cursor: ns-resize;
position: absolute;
z-index: 99;
transition: background 0.2s;
&:hover {
background: var(--primary-color-dark);
}
}
.console {
padding: 0;
padding-bottom: $footer-height;
.console-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px;
.tab-block {
margin-top: 0;
margin-bottom: 0;
}
.tab-block,
.tab-item {
background-color: transparent;
}
.tab-link {
padding: 0.2rem 0.6rem;
cursor: pointer;
white-space: nowrap;
}
}
.console-body {
overflow: auto;
display: flex;
flex-direction: column;
max-height: 100%;
padding: 0 6px 3px;
.console-log {
padding: 1px 3px;
margin: 1px 0;
border-radius: $border-radius;
user-select: text;
&-datetime {
opacity: .6;
font-size: 90%;
}
&-sql {
font-size: 95%;
opacity: 0.8;
font-weight: 700;
&:hover {
user-select: text;
}
}
&-message {
font-size: 95%;
}
&-level {
// &-log,
// &-info {}
&-warn {
color: orange;
}
&-error {
color: red;
}
}
small {
opacity: .6;
}
}
}
}
}
</style>

View File

@@ -113,7 +113,7 @@ const selectedGroup: Ref<string> = ref('manual');
const selectedMethod: Ref<string> = ref(''); const selectedMethod: Ref<string> = ref('');
const selectedValue: Ref<string> = ref(''); const selectedValue: Ref<string> = ref('');
const debounceTimeout: Ref<NodeJS.Timeout> = ref(null); const debounceTimeout: Ref<NodeJS.Timeout> = ref(null);
const methodParams: Ref<{[key: string]: string}> = ref({}); const methodParams: Ref<Record<string, string>> = ref({});
const enumArray: Ref<string[]> = ref(null); const enumArray: Ref<string[]> = ref(null);
const fakerGroups = computed(() => { const fakerGroups = computed(() => {
@@ -127,7 +127,7 @@ const fakerGroups = computed(() => {
localType.value = 'datetime'; localType.value = 'datetime';
else if (TIME.includes(props.type)) else if (TIME.includes(props.type))
localType.value = 'time'; localType.value = 'time';
else if (UUID.includes(props.type)) else if (UUID.includes(props.type) || (BLOB.includes(props.type) && props.field.key === 'pri'))
localType.value = 'uuid'; localType.value = 'uuid';
else else
localType.value = 'none'; localType.value = 'none';
@@ -177,7 +177,7 @@ const inputProps = () => {
return { type: 'text', mask: datetimeMask }; return { type: 'text', mask: datetimeMask };
} }
if (BLOB.includes(props.type)) if (BLOB.includes(props.type) && props.field.key !== 'pri')
return { type: 'file', mask: false }; return { type: 'file', mask: false };
if (BIT.includes(props.type)) if (BIT.includes(props.type))

View File

@@ -57,8 +57,22 @@
> >
<div class="panel"> <div class="panel">
<div class="panel-header p-2 text-center p-relative"> <div class="panel-header p-2 text-center p-relative">
<figure class="avatar avatar-lg pt-1 mb-1"> <figure class="avatar avatar-lg pt-1 mb-1 bg-dark">
<i class="settingbar-element-icon dbi" :class="[`dbi-${connection.client}`]" /> <div
v-if="connection.icon"
class="settingbar-connection-icon"
>
<BaseIcon
:icon-name="camelize(connection.icon)"
:type="connection.hasCustomIcon ? 'custom' : 'mdi'"
:size="42"
/>
</div>
<div
v-else
class="settingbar-element-icon dbi ml-1"
:class="[`dbi-${connection.client}`]"
/>
</figure> </figure>
<div class="panel-title h6 text-ellipsis"> <div class="panel-title h6 text-ellipsis">
{{ getConnectionName(connection.uid) }} {{ getConnectionName(connection.uid) }}
@@ -136,7 +150,19 @@
</div> </div>
</div> </div>
<div class="panel-footer text-center py-0"> <div class="panel-footer text-center py-0">
<div v-if="connection.ssl" class="chip bg-success mt-2"> <div
v-if="connection.folderName"
class="chip mt-2 bg-dark"
>
<BaseIcon
icon-name="mdiFolder"
class="mr-1"
:style="[connection.color ? `color: ${connection.color};`: '']"
:size="18"
/>
{{ connection.folderName }}
</div>
<div v-if="connection.ssl" class="chip bg-dark mt-2">
<BaseIcon <BaseIcon
icon-name="mdiShieldKey" icon-name="mdiShieldKey"
class="mr-1" class="mr-1"
@@ -144,7 +170,7 @@
/> />
SSL SSL
</div> </div>
<div v-if="connection.ssh" class="chip bg-success mt-2"> <div v-if="connection.ssh" class="chip bg-dark mt-2">
<BaseIcon <BaseIcon
icon-name="mdiConsoleNetwork" icon-name="mdiConsoleNetwork"
class="mr-1" class="mr-1"
@@ -152,7 +178,7 @@
/> />
SSH SSH
</div> </div>
<div v-if="connection.readonly" class="chip bg-success mt-2"> <div v-if="connection.readonly" class="chip bg-dark mt-2">
<BaseIcon <BaseIcon
icon-name="mdiLock" icon-name="mdiLock"
class="mr-1" class="mr-1"
@@ -209,6 +235,7 @@ import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import { useFocusTrap } from '@/composables/useFocusTrap'; import { useFocusTrap } from '@/composables/useFocusTrap';
import { camelize } from '@/libs/camelize';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -218,7 +245,9 @@ const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { connections, const { connections,
lastConnections connectionsOrder,
lastConnections,
getFolders: folders
} = storeToRefs(connectionsStore); } = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
@@ -236,7 +265,8 @@ const clients = new Map([
['mysql', 'MySQL'], ['mysql', 'MySQL'],
['maria', 'MariaDB'], ['maria', 'MariaDB'],
['pg', 'PostgreSQL'], ['pg', 'PostgreSQL'],
['sqlite', 'SQLite'] ['sqlite', 'SQLite'],
['firebird', 'Firebird SQL']
]); ]);
const searchTerm = ref(''); const searchTerm = ref('');
@@ -244,12 +274,20 @@ const isConfirmModal = ref(false);
const connectionHover: Ref<string> = ref(null); const connectionHover: Ref<string> = ref(null);
const selectedConnection: Ref<ConnectionParams> = ref(null); const selectedConnection: Ref<ConnectionParams> = ref(null);
const sortedConnections = computed(() => { const remappedConnections = computed(() => {
return connections.value return connections.value
.map(c => { .map(c => {
const connTime = lastConnections.value.find((lc) => lc.uid === c.uid)?.time || 0; const connTime = lastConnections.value.find((lc) => lc.uid === c.uid)?.time || 0;
const connIcon = connectionsOrder.value.find((co) => co.uid === c.uid).icon;
const connHasCustomIcon = connectionsOrder.value.find((co) => co.uid === c.uid).hasCustomIcon;
const folder = folders.value.find(f => f.connections.includes(c.uid));
return { return {
...c, ...c,
icon: connIcon,
color: folder?.color,
folderName: folder?.name,
hasCustomIcon: connHasCustomIcon,
time: connTime time: connTime
}; };
}) })
@@ -261,7 +299,7 @@ const sortedConnections = computed(() => {
}); });
const filteredConnections = computed(() => { const filteredConnections = computed(() => {
return sortedConnections.value.filter(connection => { return remappedConnections.value.filter(connection => {
return connection.name?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) || return connection.name?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.host?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) || connection.host?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.database?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) || connection.database?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
@@ -360,7 +398,7 @@ onBeforeUnmount(() => {
outline: none; outline: none;
&:focus { &:focus {
box-shadow: 0 0 3px 0.1rem rgba($primary-color, 80%); box-shadow: 0 0 3px 0.1rem rgba(var(--primary-color), 80%);
} }
&:hover { &:hover {

View File

@@ -73,7 +73,7 @@ const props = defineProps({
const emit = defineEmits(['confirm', 'close']); const emit = defineEmits(['confirm', 'close']);
const firstInput: Ref<HTMLInputElement[]> = ref(null); const firstInput: Ref<HTMLInputElement[]> = ref(null);
const values: Ref<{[key: string]: string}> = ref({}); const values: Ref<Record<string, string>> = ref({});
const inParameters = computed(() => { const inParameters = computed(() => {
return props.localRoutine.parameters.filter(param => param.context === 'IN'); return props.localRoutine.parameters.filter(param => param.context === 'IN');

View File

@@ -49,18 +49,46 @@
class="icon-box" class="icon-box"
:title="icon.name" :title="icon.name"
:class="[{'selected': localConnection.icon === icon.code}]" :class="[{'selected': localConnection.icon === icon.code}]"
@click="localConnection.icon = icon.code" @click="setIcon(icon.code)"
/> />
<div <div
v-else v-else
class="icon-box" class="icon-box"
:title="icon.name" :title="icon.name"
:class="[`dbi dbi-${connection.client}`, {'selected': localConnection.icon === icon.code}]" :class="[`dbi dbi-${connection.client}`, {'selected': localConnection.icon === null}]"
@click="localConnection.icon = icon.code" @click="setIcon(null)"
/> />
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ t('application.customIcon') }}</label>
</div>
<div class="col-9 icons-wrapper">
<div
v-for="icon in customIcons"
:key="icon.uid"
>
<BaseIcon
v-if="icon.uid"
:icon-name="icon.uid"
type="custom"
:size="36"
class="icon-box"
:class="[{'selected': localConnection.icon === icon.uid}]"
@click="setIcon(icon.uid, 'custom')"
@contextmenu.prevent="contextMenu($event, icon.uid)"
/>
</div>
<BaseIcon
:icon-name="'mdiPlus'"
:size="36"
class="icon-box"
@click="openFile"
/>
</div>
</div>
</form> </form>
</div> </div>
</div> </div>
@@ -74,19 +102,46 @@
</div> </div>
</div> </div>
</div> </div>
<BaseContextMenu
v-if="isContext"
:context-event="contextEvent"
@close-context="isContext = false"
>
<div class="context-element" @click="removeIconHandler">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiDelete"
:size="18"
/> {{ t('general.delete') }}</span>
</div>
</BaseContextMenu>
</Teleport> </Teleport>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia';
import { onBeforeUnmount, PropType, Ref, ref } from 'vue'; import { onBeforeUnmount, PropType, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import { useFocusTrap } from '@/composables/useFocusTrap'; import { useFocusTrap } from '@/composables/useFocusTrap';
import Application from '@/ipc-api/Application';
import { camelize } from '@/libs/camelize';
import { unproxify } from '@/libs/unproxify'; import { unproxify } from '@/libs/unproxify';
import { SidebarElement, useConnectionsStore } from '@/stores/connections'; import { SidebarElement, useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications';
const connectionsStore = useConnectionsStore(); const connectionsStore = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const { addIcon, removeIcon, updateConnectionOrder, getConnectionName } = connectionsStore;
const { customIcons } = storeToRefs(connectionsStore);
const isContext = ref(false);
const contextContent: Ref<string> = ref(null);
const contextEvent: Ref<MouseEvent> = ref(null);
const { t } = useI18n(); const { t } = useI18n();
@@ -99,8 +154,6 @@ const props = defineProps({
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const { updateConnectionOrder, getConnectionName } = connectionsStore;
const icons = [ const icons = [
{ name: 'default', code: null }, { name: 'default', code: null },
@@ -160,14 +213,77 @@ const editFolderAppearance = () => {
closeModal(); closeModal();
}; };
const camelize = (text: string) => { const setIcon = (code: string, type?: 'mdi' | 'custom') => {
const textArr = text.split('-'); localConnection.value.icon = code;
for (let i = 0; i < textArr.length; i++) { localConnection.value.hasCustomIcon = type === 'custom';
if (i === 0) continue; };
textArr[i] = textArr[i].charAt(0).toUpperCase() + textArr[i].slice(1);
}
return textArr.join(''); const removeIconHandler = () => {
if (localConnection.value.icon === contextContent.value) {
setIcon(null);
updateConnectionOrder(localConnection.value);
}
removeIcon(contextContent.value);
isContext.value = false;
};
const adjustSVGContent = (svgContent: string) => {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(svgContent, 'image/svg+xml');
const parseError = doc.querySelector('parsererror');
if (parseError) {
addNotification({ status: 'error', message: parseError.textContent });
return null;
}
const svg = doc.documentElement;
if (svg.tagName.toLowerCase() !== 'svg') {
addNotification({ status: 'error', message: t('application.invalidFIle') });
return null;
}
if (!svg.hasAttribute('viewBox')) {
const width = svg.getAttribute('width') || '36';
const height = svg.getAttribute('height') || '36';
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
}
svg.removeAttribute('width');
svg.removeAttribute('height');
const serializer = new XMLSerializer();
return serializer.serializeToString(svg);
}
catch (error) {
addNotification({ status: 'error', message: error.stack });
return null;
}
};
const openFile = async () => {
const result = await Application.showOpenDialog({
properties: ['openFile'],
filters: [{ name: '"SVG"', extensions: ['svg'] }]
});
if (result && !result.canceled) {
const file = result.filePaths[0];
let content = await Application.readFile({ filePath: file, encoding: 'utf-8' });
content = adjustSVGContent(content);
const base64Content = Buffer.from(content).toString('base64');
addIcon(base64Content);
}
};
const contextMenu = (event: MouseEvent, iconUid: string) => {
contextEvent.value = event;
contextContent.value = iconUid;
isContext.value = true;
}; };
const closeModal = () => emit('close'); const closeModal = () => emit('close');
@@ -204,7 +320,7 @@ onBeforeUnmount(() => {
cursor: pointer; cursor: pointer;
&.selected { &.selected {
outline: 2px solid $primary-color; outline: 2px solid var(--primary-color);
border-radius: 8px; border-radius: 8px;
} }
} }

View File

@@ -282,7 +282,7 @@
import { ClientCode, SchemaInfos } from 'common/interfaces/antares'; import { ClientCode, SchemaInfos } from 'common/interfaces/antares';
import { Customizations } from 'common/interfaces/customizations'; import { Customizations } from 'common/interfaces/customizations';
import { ExportOptions, ExportState } from 'common/interfaces/exporter'; import { ExportOptions, ExportState } from 'common/interfaces/exporter';
import { ipcRenderer } from 'electron'; import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as moment from 'moment'; import * as moment from 'moment';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, Ref, ref } from 'vue'; import { computed, onBeforeUnmount, Ref, ref } from 'vue';
@@ -293,6 +293,7 @@ import BaseSelect from '@/components/BaseSelect.vue';
import { useFocusTrap } from '@/composables/useFocusTrap'; 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 { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useSchemaExportStore } from '@/stores/schemaExport'; import { useSchemaExportStore } from '@/stores/schemaExport';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -327,7 +328,7 @@ const tables: Ref<{
}[]> = ref([]); }[]> = ref([]);
const options: Ref<Partial<ExportOptions>> = ref({ const options: Ref<Partial<ExportOptions>> = ref({
schema: selectedSchema.value, schema: selectedSchema.value,
includes: {} as {[key: string]: boolean}, includes: {} as Record<string, boolean>,
outputFormat: 'sql' as 'sql' | 'sql.zip', outputFormat: 'sql' as 'sql' | 'sql.zip',
sqlInsertAfter: 250, sqlInsertAfter: 250,
sqlInsertDivider: 'bytes' as 'bytes' | 'rows' sqlInsertDivider: 'bytes' as 'bytes' | 'rows'
@@ -379,21 +380,34 @@ const startExport = async () => {
try { try {
const { status, response } = await Schema.export(params); const { status, response } = await Schema.export(params);
if (status === 'success') if (status === 'success')
progressStatus.value = response.cancelled ? t('general.aborted') : t('general.completed'); progressStatus.value = response.cancelled ? t('general.aborted') : t('general.completed');
else { else {
progressStatus.value = response; progressStatus.value = response;
addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: response,
date: new Date()
});
} }
} }
catch (err) { catch (err) {
addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: err.stack,
date: new Date()
});
} }
isExporting.value = false; isExporting.value = false;
}; };
const updateProgress = (event: Event, state: ExportState) => { const updateProgress = (event: IpcRendererEvent, state: ExportState) => {
progressPercentage.value = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1)); progressPercentage.value = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) { switch (state.op) {
case 'PROCESSING': case 'PROCESSING':

View File

@@ -142,7 +142,7 @@ const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { trapRef } = useFocusTrap({ disableAutofocus: true }); const { trapRef } = useFocusTrap({ disableAutofocus: true });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const localRow: Ref<{[key: string]: any}> = ref({}); const localRow: Ref<Record<string, any>> = ref({});
const fieldsToExclude = ref([]); const fieldsToExclude = ref([]);
const nInserts = ref(1); const nInserts = ref(1);
const isInserting = ref(false); const isInserting = ref(false);
@@ -225,7 +225,7 @@ const insertRows = async () => {
delete rowToInsert[key]; delete rowToInsert[key];
}); });
const fieldTypes: {[key: string]: string} = {}; const fieldTypes: Record<string, string> = {};
props.fields.forEach(field => { props.fields.forEach(field => {
fieldTypes[field.name] = field.type; fieldTypes[field.name] = field.type;
}); });
@@ -290,7 +290,7 @@ onMounted(() => {
} }
}, 50); }, 50);
const rowObj: {[key: string]: unknown} = {}; const rowObj: Record<string, unknown> = {};
if (!props.rowToDuplicate) { if (!props.rowToDuplicate) {
// Set default values // Set default values
@@ -339,6 +339,8 @@ onMounted(() => {
for (const field of props.fields) { for (const field of props.fields) {
if (typeof props.rowToDuplicate[field.name] !== 'object') if (typeof props.rowToDuplicate[field.name] !== 'object')
rowObj[field.name] = { value: props.rowToDuplicate[field.name] }; rowObj[field.name] = { value: props.rowToDuplicate[field.name] };
else if (field.type === 'JSON')
rowObj[field.name] = { value: JSON.stringify(props.rowToDuplicate[field.name]) };
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
fieldsToExclude.value = [...fieldsToExclude.value, field.name]; fieldsToExclude.value = [...fieldsToExclude.value, field.name];

View File

@@ -75,7 +75,7 @@
<code <code
class="cut-text" class="cut-text"
:title="query.sql" :title="query.sql"
v-html="highlight(highlightWord(query.sql), {html: true})" v-html="highlight(query.sql, {html: true})"
/> />
</div> </div>
<div class="tile-bottom-content"> <div class="tile-bottom-content">
@@ -115,7 +115,7 @@
<BaseIcon icon-name="mdiHistory" :size="48" /> <BaseIcon icon-name="mdiHistory" :size="48" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ t('database.thereIsNoQueriesYet') }} {{ t('database.thereAreNoQueriesYet') }}
</p> </p>
</div> </div>
</div> </div>
@@ -210,17 +210,6 @@ const resizeResults = () => {
const refreshScroller = () => resizeResults(); const refreshScroller = () => resizeResults();
const closeModal = () => emit('close'); 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) => { const onKey = (e: KeyboardEvent) => {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') if (e.key === 'Escape')
@@ -287,7 +276,7 @@ onBeforeUnmount(() => {
max-width: 100%; max-width: 100%;
display: inline-block; display: inline-block;
font-size: 100%; font-size: 100%;
// color: $primary-color; // color: var(--primary-color);
opacity: 0.8; opacity: 0.8;
font-weight: 600; font-weight: 600;
} }

View File

@@ -55,7 +55,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ImportState } from 'common/interfaces/importer'; import { ImportState } from 'common/interfaces/importer';
import { ipcRenderer } from 'electron'; import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as moment from 'moment'; import * as moment from 'moment';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, Ref, ref } from 'vue'; import { computed, onBeforeUnmount, Ref, ref } from 'vue';
@@ -63,6 +63,7 @@ import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -118,23 +119,35 @@ const startImport = async (file: string) => {
else { else {
progressStatus.value = response; progressStatus.value = response;
addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: response,
date: new Date()
});
} }
refreshSchema({ uid, schema: props.selectedSchema }); refreshSchema({ uid, schema: props.selectedSchema });
completed.value = true; completed.value = true;
} }
catch (err) { catch (err) {
addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: err.stack,
date: new Date()
});
} }
isImporting.value = false; isImporting.value = false;
}; };
const updateProgress = (event: Event, state: ImportState) => { const updateProgress = (event: IpcRendererEvent, state: ImportState) => {
progressPercentage.value = parseFloat(Number(state.percentage).toFixed(1)); progressPercentage.value = parseFloat(Number(state.percentage).toFixed(1));
queryCount.value = Number(state.queryCount); queryCount.value = Number(state.queryCount);
}; };
const handleQueryError = (event: Event, err: { time: string; message: string }) => { const handleQueryError = (event: IpcRendererEvent, err: { time: string; message: string }) => {
queryErrors.value.push(err); queryErrors.value.push(err);
}; };

View File

@@ -1,6 +1,6 @@
<template> <template>
<ConfirmModal <ConfirmModal
size="medium" size="resize"
:disable-autofocus="true" :disable-autofocus="true"
:close-on-confirm="!!localNote.note.length" :close-on-confirm="!!localNote.note.length"
:confirm-text="t('general.save')" :confirm-text="t('general.save')"
@@ -52,6 +52,10 @@
v-model="localNote.note" v-model="localNote.note"
:mode="editorMode" :mode="editorMode"
:show-line-numbers="false" :show-line-numbers="false"
:auto-focus="true"
:height="400"
:width="640"
:resizable="true"
/> />
</div> </div>
</form> </form>

View File

@@ -1,6 +1,6 @@
<template> <template>
<ConfirmModal <ConfirmModal
size="medium" size="resize"
:disable-autofocus="true" :disable-autofocus="true"
:close-on-confirm="!!newNote.note.length" :close-on-confirm="!!newNote.note.length"
:confirm-text="t('general.save')" :confirm-text="t('general.save')"
@@ -52,6 +52,10 @@
v-model="newNote.note" v-model="newNote.note"
:mode="editorMode" :mode="editorMode"
:show-line-numbers="false" :show-line-numbers="false"
:auto-focus="true"
:height="400"
:width="640"
:resizable="true"
/> />
</div> </div>
</form> </form>

View File

@@ -67,7 +67,7 @@ const props = defineProps({
const emit = defineEmits(['select-row', 'contextmenu', 'stop-refresh']); const emit = defineEmits(['select-row', 'contextmenu', 'stop-refresh']);
const isInlineEditor: Ref<{[key: string]: boolean}> = ref({}); const isInlineEditor: Ref<Record<string, boolean>> = ref({});
const isInfoModal = ref(false); const isInfoModal = ref(false);
const editorMode = ref('sql'); const editorMode = ref('sql');

View File

@@ -612,7 +612,7 @@ const otherContributors = computed(() => {
return contributors return contributors
.split(',') .split(',')
.filter(c => !c.includes(appAuthor)) .filter(c => !c.includes(appAuthor))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); .sort((a, b) => a.toLowerCase().trim().localeCompare(b.toLowerCase()));
}); });
const selectTab = (tab: string) => { const selectTab = (tab: string) => {
@@ -703,7 +703,7 @@ onBeforeUnmount(() => {
&.selected { &.selected {
img { img {
box-shadow: 0 0 0 3px $primary-color; box-shadow: 0 0 0 3px var(--primary-color);
} }
} }
@@ -731,7 +731,7 @@ onBeforeUnmount(() => {
.badge-update::after { .badge-update::after {
bottom: initial; bottom: initial;
background: $primary-color; background: var(--primary-color);
} }
.form-label { .form-label {

View File

@@ -169,7 +169,7 @@ const emit = defineEmits(['close']);
const { trapRef } = useFocusTrap(); const { trapRef } = useFocusTrap();
const { getConnectionName } = useConnectionsStore(); const { getConnectionName } = useConnectionsStore();
const { connectionsOrder, connections } = storeToRefs(useConnectionsStore()); const { connectionsOrder, connections, customIcons } = storeToRefs(useConnectionsStore());
const localConnections = unproxify<ConnectionParams[]>(connections.value); const localConnections = unproxify<ConnectionParams[]>(connections.value);
const localConnectionsOrder = unproxify<SidebarElement[]>(connectionsOrder.value); const localConnectionsOrder = unproxify<SidebarElement[]>(connectionsOrder.value);
@@ -246,7 +246,8 @@ const exportData = () => {
const exportObj = encrypt(JSON.stringify({ const exportObj = encrypt(JSON.stringify({
connections: filteredConnections, connections: filteredConnections,
connectionsOrder: filteredOrders connectionsOrder: filteredOrders,
customIcons: customIcons.value
}), options.value.passkey); }), options.value.passkey);
// console.log(exportObj, JSON.parse(decrypt(exportObj, options.value.passkey))); // console.log(exportObj, JSON.parse(decrypt(exportObj, options.value.passkey)));

View File

@@ -103,7 +103,7 @@ import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import BaseUploadInput from '@/components/BaseUploadInput.vue'; import BaseUploadInput from '@/components/BaseUploadInput.vue';
import { unproxify } from '@/libs/unproxify'; import { unproxify } from '@/libs/unproxify';
import { SidebarElement, useConnectionsStore } from '@/stores/connections'; import { CustomIcon, SidebarElement, useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
const { t } = useI18n(); const { t } = useI18n();
@@ -156,6 +156,7 @@ const importData = () => {
const importObj: { const importObj: {
connections: ConnectionParams[]; connections: ConnectionParams[];
connectionsOrder: SidebarElement[]; connectionsOrder: SidebarElement[];
customIcons: CustomIcon[];
} = JSON.parse(decrypt(hash, options.value.passkey)); } = JSON.parse(decrypt(hash, options.value.passkey));
if (options.value.ignoreDuplicates) { if (options.value.ignoreDuplicates) {
@@ -205,7 +206,6 @@ const importData = () => {
.includes(c.uid) || .includes(c.uid) ||
(c.isFolder && c.connections.every(c => newConnectionsUid.includes(c)))); (c.isFolder && c.connections.every(c => newConnectionsUid.includes(c))));
} }
importConnections(importObj); importConnections(importObj);
addNotification({ addNotification({
@@ -215,6 +215,7 @@ const importData = () => {
closeModal(); closeModal();
} }
catch (error) { catch (error) {
console.error(error);
addNotification({ addNotification({
status: 'error', status: 'error',
message: t('application.wrongImportPassword') message: t('application.wrongImportPassword')
@@ -222,6 +223,7 @@ const importData = () => {
} }
} }
catch (error) { catch (error) {
console.error(error);
addNotification({ addNotification({
status: 'error', status: 'error',
message: t('application.wrongFileFormat') message: t('application.wrongFileFormat')

View File

@@ -42,7 +42,7 @@
tabindex="0" tabindex="0"
> >
<div class="td py-1"> <div class="td py-1">
{{ t(shortcutEvents[shortcut.event].l18n, {param: shortcutEvents[shortcut.event].l18nParam}) }} {{ t(shortcutEvents[shortcut.event].i18n, {param: shortcutEvents[shortcut.event].i18nParam}) }}
</div> </div>
<div <div
class="td py-1" class="td py-1"
@@ -167,7 +167,7 @@
</template> </template>
<template #body> <template #body>
<div class="mb-2"> <div class="mb-2">
{{ t('general.deleteConfirm') }} <b>{{ t(shortcutEvents[shortcutToDelete.event].l18n, {param: shortcutEvents[shortcutToDelete.event].l18nParam}) }} (<span v-html="parseKeys(shortcutToDelete.keys)" />)</b>? {{ t('general.deleteConfirm') }} <b>{{ t(shortcutEvents[shortcutToDelete.event].i18n, {param: shortcutEvents[shortcutToDelete.event].i18nParam}) }} (<span v-html="parseKeys(shortcutToDelete.keys)" />)</b>?
</div> </div>
</template> </template>
</ConfirmModal> </ConfirmModal>
@@ -233,7 +233,7 @@ const { shortcuts } = storeToRefs(settingsStore);
const eventOptions = computed(() => { const eventOptions = computed(() => {
return Object.keys(shortcutEvents) return Object.keys(shortcutEvents)
.map(key => { .map(key => {
return { value: key, label: t(shortcutEvents[key].l18n, { param: shortcutEvents[key].l18nParam }) }; return { value: key, label: t(shortcutEvents[key].i18n, { param: shortcutEvents[key].i18nParam }) };
}) })
.sort((a, b) => { .sort((a, b) => {
if (a.label < b.label) return -1; if (a.label < b.label) return -1;

View File

@@ -3,6 +3,7 @@
<div <div
:id="`editor-${id}`" :id="`editor-${id}`"
class="editor" class="editor"
:class="editorClasses"
:style="{height: `${height}px`}" :style="{height: `${height}px`}"
/> />
</div> </div>
@@ -54,7 +55,8 @@ const props = defineProps({
schema: { type: String, default: '' }, schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false }, autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false }, readOnly: { type: Boolean, default: false },
height: { type: Number, default: 200 } height: { type: Number, default: 200 },
editorClasses: { type: String, default: '' }
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
@@ -341,7 +343,7 @@ onMounted(() => {
lastTableFields.value = res.response.map((field: { name: string }) => field.name); lastTableFields.value = res.response.map((field: { name: string }) => field.name);
editor.value.completers = [tableFieldsCompleter.value]; editor.value.completers = [tableFieldsCompleter.value];
editor.value.execCommand('startAutocomplete'); editor.value.execCommand('startAutocomplete');
}).catch(console.log); }).catch(console.error);
} }
else else
editor.value.completers = customCompleter.value; editor.value.completers = customCompleter.value;
@@ -405,18 +407,17 @@ defineExpose({ editor });
.ace_gutter-cell.ace_breakpoint { .ace_gutter-cell.ace_breakpoint {
&::before { &::before {
content: '\F0403'; content: '';
position: absolute; position: absolute;
left: 3px; left: 0px;
top: 2px; top: 8px;
color: $primary-color;
display: inline-block; display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons", sans-serif; width: 0;
font-size: inherit; height: 0;
text-rendering: auto; border-left: 8px solid transparent;
line-height: inherit; border-top: 8px solid transparent;
-webkit-font-smoothing: antialiased; border-right: 8px solid var(--primary-color);
-moz-osx-font-smoothing: grayscale; transform: rotate(-45deg);
} }
} }
</style> </style>

View File

@@ -14,7 +14,7 @@
<div class="tile-icon"> <div class="tile-icon">
<BaseIcon <BaseIcon
:icon-name="note.type === 'query' :icon-name="note.type === 'query'
? 'mdiStarOutline' ? 'mdiHeartOutline'
: note.type === 'todo' : note.type === 'todo'
? note.isArchived ? note.isArchived
? 'mdiCheckboxMarkedOutline' ? 'mdiCheckboxMarkedOutline'
@@ -32,7 +32,7 @@
v-if="note.type === 'query'" v-if="note.type === 'query'"
ref="noteParagraph" ref="noteParagraph"
class="tile-paragraph sql" class="tile-paragraph sql"
v-html="highlight(highlightWord(note.note), {html: true})" v-html="highlight(note.note, {html: true})"
/> />
<div <div
v-else v-else
@@ -47,65 +47,50 @@
<div class="tile-history-buttons"> <div class="tile-history-buttons">
<button <button
v-if="note.type === 'todo' && !note.isArchived" v-if="note.type === 'todo' && !note.isArchived"
class="btn btn-link pl-1" class="btn btn-dark tooltip tooltip-left"
:data-tooltip="t('general.archive')"
@click.stop="$emit('archive-note', note.uid)" @click.stop="$emit('archive-note', note.uid)"
> >
<BaseIcon <BaseIcon icon-name="mdiCheck" :size="22" />
icon-name="mdiCheck"
class="pr-1"
:size="22"
/> {{ t('general.archive') }}
</button> </button>
<button <button
v-if="note.type === 'todo' && note.isArchived" v-if="note.type === 'todo' && note.isArchived"
class="btn btn-link pl-1" class="btn btn-dark tooltip tooltip-left"
:data-tooltip="t('general.undo')"
@click.stop="$emit('restore-note', note.uid)" @click.stop="$emit('restore-note', note.uid)"
> >
<BaseIcon <BaseIcon icon-name="mdiRestore" :size="22" />
icon-name="mdiRestore"
class="pr-1"
:size="22"
/> {{ t('general.undo') }}
</button> </button>
<button <button
v-if="note.type === 'query'" v-if="note.type === 'query'"
class="btn btn-link pl-1" class="btn btn-dark tooltip tooltip-left"
:data-tooltip="t('general.select')"
@click.stop="$emit('select-query', note.note)" @click.stop="$emit('select-query', note.note)"
> >
<BaseIcon <BaseIcon icon-name="mdiOpenInApp" :size="22" />
icon-name="mdiOpenInApp"
class="pr-1"
:size="22"
/> {{ t('general.select') }}
</button> </button>
<button <button
v-if="note.type === 'query'" v-if="note.type === 'query'"
class="btn btn-link pl-1" class="btn btn-dark tooltip tooltip-left"
:data-tooltip="t('general.copy')"
@click.stop="copyText(note.note)" @click.stop="copyText(note.note)"
> >
<BaseIcon <BaseIcon icon-name="mdiContentCopy" :size="18" />
icon-name="mdiContentCopy"
class="pr-1"
:size="22"
/> {{ t('general.copy') }}
</button> </button>
<button <button
v-if=" !note.isArchived" v-if=" !note.isArchived"
class="btn btn-link pl-1" class="btn btn-dark tooltip tooltip-left"
:data-tooltip="t('general.edit')"
@click.stop="$emit('edit-note')" @click.stop="$emit('edit-note')"
> >
<BaseIcon <BaseIcon icon-name="mdiPencil" :size="22" />
icon-name="mdiPencil"
class="pr-1"
:size="22"
/> {{ t('general.edit') }}
</button> </button>
<button class="btn btn-link pl-1" @click.stop="$emit('delete-note', note.uid)"> <button
<BaseIcon class="btn btn-dark tooltip tooltip-left"
icon-name="mdiDeleteForever" :data-tooltip="t('general.delete')"
class="pr-1" @click.stop="$emit('delete-note', note.uid)"
:size="22" >
/> {{ t('general.delete') }} <BaseIcon icon-name="mdiDeleteForever" :size="22" />
</button> </button>
</div> </div>
</div> </div>
@@ -256,9 +241,9 @@ const highlightWord = (string: string) => {
code, pre { code, pre {
max-width: 100%; max-width: 100%;
width: 100%;
display: inline-block; display: inline-block;
font-size: 100%; font-size: 100%;
// color: $primary-color;
opacity: 0.8; opacity: 0.8;
font-weight: 600; font-weight: 600;
white-space: break-spaces; white-space: break-spaces;
@@ -278,11 +263,14 @@ const highlightWord = (string: string) => {
button { button {
font-size: 0.7rem; font-size: 0.7rem;
height: 1rem;
line-height: 1rem; line-height: 1rem;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0 5px;
padding: 0;
height: 24px;
width: 24px;
} }
} }
} }
@@ -342,6 +330,8 @@ const highlightWord = (string: string) => {
<style lang="scss"> <style lang="scss">
.tile-paragraph { .tile-paragraph {
white-space: initial; white-space: initial;
word-break: break-word;
user-select: text;
h1, h2, h3, h4, h5, h6, p, li { h1, h2, h3, h4, h5, h6, p, li {
margin: 0; margin: 0;

View File

@@ -56,6 +56,7 @@
> >
<BaseIcon <BaseIcon
:icon-name="camelize(element.icon)" :icon-name="camelize(element.icon)"
:type="element.hasCustomIcon ? 'custom' : 'mdi'"
:size="36" :size="36"
/> />
</div> </div>
@@ -93,6 +94,7 @@ import * as Draggable from 'vuedraggable';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import SettingBarConnectionsFolder from '@/components/SettingBarConnectionsFolder.vue'; import SettingBarConnectionsFolder from '@/components/SettingBarConnectionsFolder.vue';
import { camelize } from '@/libs/camelize';
import { SidebarElement, useConnectionsStore } from '@/stores/connections'; import { SidebarElement, useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -165,16 +167,6 @@ const getStatusBadge = (uid: string) => {
} }
}; };
const camelize = (text: string) => {
const textArr = text.split('-');
for (let i = 0; i < textArr.length; i++) {
if (i === 0) continue;
textArr[i] = textArr[i].charAt(0).toUpperCase() + textArr[i].slice(1);
}
return textArr.join('');
};
watch(() => dummyNested.value.length, () => { watch(() => dummyNested.value.length, () => {
dummyNested.value = []; dummyNested.value = [];
}); });

View File

@@ -70,6 +70,7 @@
> >
<BaseIcon <BaseIcon
:icon-name="camelize(getConnectionOrderByUid(element).icon)" :icon-name="camelize(getConnectionOrderByUid(element).icon)"
:type="getConnectionOrderByUid(element).hasCustomIcon ? 'custom' : 'mdi'"
:size="36" :size="36"
/> />
</div> </div>

View File

@@ -15,6 +15,56 @@
:size="18" :size="18"
/> {{ t('connection.disconnect') }}</span> /> {{ t('connection.disconnect') }}</span>
</div> </div>
<div v-if="!contextConnection.isFolder" class="context-element">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiFolderMove"
:size="18"
/> {{ t('general.moveTo') }}</span>
<BaseIcon
class="text-light ml-1"
icon-name="mdiChevronRight"
:size="18"
/>
<div class="context-submenu">
<div class="context-element" @click.stop="moveToFolder(null)">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiFolderPlus"
:size="18"
/> {{ t('application.newFolder') }}</span>
</div>
<div
v-for="folder in filteredFolders"
:key="folder.uid"
class="context-element"
@click.stop="moveToFolder(folder.uid)"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiFolder"
:size="18"
:style="`color: ${folder.color}!important`"
/> {{ folder.name || t('general.folder') }}</span>
</div>
<div
v-if="isInFolder"
class="context-element"
@click="outOfFolder"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiFolderOff"
:size="18"
/> {{ t('application.outOfFolder') }}</span>
</div>
</div>
</div>
<div class="context-element" @click.stop="showAppearanceModal"> <div class="context-element" @click.stop="showAppearanceModal">
<span class="d-flex"> <span class="d-flex">
<BaseIcon <BaseIcon
@@ -79,6 +129,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { storeToRefs } from 'pinia';
import { computed, Prop, ref } from 'vue'; import { computed, Prop, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -98,9 +149,14 @@ const {
getConnectionByUid, getConnectionByUid,
getConnectionName, getConnectionName,
addConnection, addConnection,
deleteConnection deleteConnection,
addFolder,
addToFolder,
removeFromFolders
} = connectionsStore; } = connectionsStore;
const { getFolders: folders } = storeToRefs(connectionsStore);
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { const {
@@ -121,6 +177,8 @@ const isConnectionEdit = ref(false);
const connectionName = computed(() => props.contextConnection.name || getConnectionName(props.contextConnection.uid) || t('general.folder', 1)); const connectionName = computed(() => props.contextConnection.name || getConnectionName(props.contextConnection.uid) || t('general.folder', 1));
const isConnected = computed(() => getWorkspace(props.contextConnection.uid)?.connectionStatus === 'connected'); const isConnected = computed(() => getWorkspace(props.contextConnection.uid)?.connectionStatus === 'connected');
const filteredFolders = computed(() => folders.value.filter(f => !f.connections.includes(props.contextConnection.uid)));
const isInFolder = computed(() => folders.value.some(f => f.connections.includes(props.contextConnection.uid)));
const confirmDeleteConnection = () => { const confirmDeleteConnection = () => {
if (isConnected.value) if (isConnected.value)
@@ -129,6 +187,27 @@ const confirmDeleteConnection = () => {
closeContext(); closeContext();
}; };
const moveToFolder = (folderUid?: string) => {
if (!folderUid) {
addFolder({
connections: [props.contextConnection.uid]
});
}
else {
addToFolder({
folder: folderUid,
connection: props.contextConnection.uid
});
}
closeContext();
};
const outOfFolder = () => {
removeFromFolders(props.contextConnection.uid);
closeContext();
};
const duplicateConnection = () => { const duplicateConnection = () => {
let connectionCopy = getConnectionByUid(props.contextConnection.uid); let connectionCopy = getConnectionByUid(props.contextConnection.uid);
connectionCopy = { connectionCopy = {

View File

@@ -1,8 +1,8 @@
<template> <template>
<div <div
id="footer" id="footer"
:class="[lightColors.includes(footerColor) ? 'text-dark' : 'text-light']" :class="[lightColors.includes(accentColor) ? 'text-dark' : 'text-light']"
:style="`background-color: ${footerColor};`" :style="`background-color: ${accentColor};`"
> >
<div class="footer-left-elements"> <div class="footer-left-elements">
<ul class="footer-elements"> <ul class="footer-elements">
@@ -43,11 +43,7 @@
<div class="footer-right-elements"> <div class="footer-right-elements">
<ul class="footer-elements"> <ul class="footer-elements">
<li <li class="footer-element footer-link" @click="toggleConsole()">
v-if="workspace?.connectionStatus === 'connected'"
class="footer-element footer-link"
@click="toggleConsole()"
>
<BaseIcon <BaseIcon
icon-name="mdiConsoleLine" icon-name="mdiConsoleLine"
class="mr-1" class="mr-1"
@@ -85,10 +81,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { shell } from 'electron'; import { shell } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed, ComputedRef } from 'vue'; import { computed, ComputedRef, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import { hexToRGBA } from '@/libs/hexToRgba';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useConsoleStore } from '@/stores/console'; import { useConsoleStore } from '@/stores/console';
@@ -117,7 +114,11 @@ const { getWorkspace } = workspacesStore;
const { getConnectionFolder, getConnectionByUid } = connectionsStore; const { getConnectionFolder, getConnectionByUid } = connectionsStore;
const workspace = computed(() => getWorkspace(workspaceUid.value)); const workspace = computed(() => getWorkspace(workspaceUid.value));
const footerColor = computed(() => getConnectionFolder(workspaceUid.value)?.color || '#E36929'); const accentColor = computed(() => {
if (getConnectionFolder(workspaceUid.value)?.color)
return getConnectionFolder(workspaceUid.value).color;
return '#E36929';
});
const connectionInfos = computed(() => getConnectionByUid(workspaceUid.value)); const connectionInfos = computed(() => getConnectionByUid(workspaceUid.value));
const version: ComputedRef<DatabaseInfos> = computed(() => { const version: ComputedRef<DatabaseInfos> = computed(() => {
return getWorkspace(workspaceUid.value) ? workspace.value.version : null; return getWorkspace(workspaceUid.value) ? workspace.value.version : null;
@@ -129,7 +130,17 @@ const versionString = computed(() => {
return ''; return '';
}); });
watch(accentColor, () => {
changeAccentColor();
});
const openOutside = (link: string) => shell.openExternal(link); const openOutside = (link: string) => shell.openExternal(link);
const changeAccentColor = () => {
document.querySelector<HTMLBodyElement>(':root').style.setProperty('--primary-color', accentColor.value);
document.querySelector<HTMLBodyElement>(':root').style.setProperty('--primary-color-shadow', hexToRGBA(accentColor.value, 0.2));
};
changeAccentColor();
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -32,7 +32,7 @@ const { removeNotification } = notificationsStore;
const { notifications } = storeToRefs(notificationsStore); const { notifications } = storeToRefs(notificationsStore);
const { notificationsTimeout } = storeToRefs(settingsStore); const { notificationsTimeout } = storeToRefs(settingsStore);
const timeouts: Ref<{[key: string]: NodeJS.Timeout}> = ref({}); const timeouts: Ref<Record<string, NodeJS.Timeout>> = ref({});
const latestNotifications = computed(() => notifications.value.slice(0, 10)); const latestNotifications = computed(() => notifications.value.slice(0, 10));

View File

@@ -186,7 +186,7 @@ if (!connectionsArr.value.length)
.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}); width: 100%;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 3px; width: 3px;
@@ -233,6 +233,7 @@ if (!connectionsArr.value.length)
border-radius: 0; border-radius: 0;
padding: 0; padding: 0;
position: relative; position: relative;
border: none;
&:hover { &:hover {
opacity: 1; opacity: 1;

View File

@@ -1,6 +1,5 @@
<template> <template>
<div <div
v-if="!isLinux"
id="titlebar" id="titlebar"
@dblclick="toggleFullScreen" @dblclick="toggleFullScreen"
> >
@@ -21,16 +20,27 @@
class="titlebar-element" class="titlebar-element"
@click="openDevTools" @click="openDevTools"
> >
<BaseIcon icon-name="mdiBugPlayOutline" :size="24" /> <BaseIcon icon-name="mdiBugPlayOutline" :size="18" />
</div> </div>
<div <div
v-if="isDevelopment" v-if="isDevelopment"
class="titlebar-element" class="titlebar-element"
@click="reload" @click="reload"
> >
<BaseIcon icon-name="mdiRefresh" :size="24" /> <BaseIcon icon-name="mdiRefresh" :size="18" />
</div> </div>
<div v-if="isWindows" :style="'width: 140px;'" /> <div v-if="isWindows" :style="'width: 140px;'" />
<div v-if="isLinux" class="d-flex">
<div class="titlebar-element" @click="minimize">
<BaseIcon icon-name="mdiWindowMinimize" :size="18" />
</div>
<div class="titlebar-element" @click="toggleFullScreen">
<BaseIcon :icon-name="isMaximized ? 'mdiWindowRestore' : 'mdiWindowMaximize'" :size="18" />
</div>
<div class="titlebar-element" @click="closeApp">
<BaseIcon icon-name="mdiClose" :size="18" />
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -74,6 +84,18 @@ const windowTitle = computed(() => {
return [connectionName, ...breadcrumbs].join(' • '); return [connectionName, ...breadcrumbs].join(' • ');
}); });
const openDevTools = () => {
w.value.webContents.openDevTools();
};
const reload = () => {
w.value.reload();
};
const minimize = () => {
w.value.minimize();
};
const toggleFullScreen = () => { const toggleFullScreen = () => {
if (isMaximized.value) if (isMaximized.value)
w.value.unmaximize(); w.value.unmaximize();
@@ -81,12 +103,8 @@ const toggleFullScreen = () => {
w.value.maximize(); w.value.maximize();
}; };
const openDevTools = () => { const closeApp = () => {
w.value.webContents.openDevTools(); ipcRenderer.send('close-app');
};
const reload = () => {
w.value.reload();
}; };
const onResize = () => { const onResize = () => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,432 +1,461 @@
<template> <template>
<div class="connection-panel"> <div class="connection-panel-wrapper p-relative">
<div class="panel"> <div class="connection-panel">
<div class="panel-nav"> <div class="panel">
<ul class="tab tab-block"> <div class="panel-nav">
<li <ul class="tab tab-block">
class="tab-item c-hand" <li
:class="{'active': selectedTab === 'general'}" class="tab-item c-hand"
@click="selectTab('general')" :class="{'active': selectedTab === 'general'}"
> @click="selectTab('general')"
<a class="tab-link">{{ t('application.general') }}</a> >
</li> <a class="tab-link">{{ t('application.general') }}</a>
<li </li>
v-if="clientCustomizations.sslConnection" <li
class="tab-item c-hand" v-if="clientCustomizations.sslConnection"
:class="{'active': selectedTab === 'ssl'}" class="tab-item c-hand"
@click="selectTab('ssl')" :class="{'active': selectedTab === 'ssl'}"
> @click="selectTab('ssl')"
<a class="tab-link">{{ t('connection.ssl') }}</a> >
</li> <a class="tab-link">{{ t('connection.ssl') }}</a>
<li </li>
v-if="clientCustomizations.sshConnection" <li
class="tab-item c-hand" v-if="clientCustomizations.sshConnection"
:class="{'active': selectedTab === 'ssh'}" class="tab-item c-hand"
@click="selectTab('ssh')" :class="{'active': selectedTab === 'ssh'}"
> @click="selectTab('ssh')"
<a class="tab-link">{{ t('connection.sshTunnel') }}</a> >
</li> <a class="tab-link">{{ t('connection.sshTunnel') }}</a>
</ul> </li>
</div> </ul>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionName') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.client') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseSelect
v-model="connection.client"
:options="clients"
option-track-by="slug"
option-label="name"
class="form-select"
/>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionString') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="pgString"
v-model="connection.pgConnString"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.databasePath"
:message="t('general.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
:placeholder="clientCustomizations.defaultDatabase"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.schema') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.schema"
class="form-input"
type="text"
:placeholder="t('general.all')"
>
</div>
</div>
<div v-if="clientCustomizations.readOnlyMode" class="form-group columns">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }}
</label>
</div>
</div>
</fieldset>
</form>
</div> </div>
</div> <div v-if="selectedTab === 'general'" class="panel-body py-0">
<div v-if="selectedTab === 'ssl'" class="panel-body py-0"> <div>
<div> <form class="form-horizontal">
<form class="form-horizontal"> <fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-5 col-sm-12"> <div class="column col-5 col-sm-12">
<label class="form-label cut-text"> <label class="form-label cut-text">{{ t('connection.connectionName') }}</label>
{{ t('connection.enableSsl') }} </div>
</label> <div class="column col-7 col-sm-12">
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="connection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.key"
:message="t('general.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.certificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.cert"
:message="t('general.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.caCertificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.ca"
:message="t('general.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.ciphers') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('connection.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsh') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="connection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.sshKey"
:message="t('general.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.passphrase') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPassphrase"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.keepAliveInterval') }}</label>
</div>
<div class="column col-7 col-sm-12">
<div class="input-group">
<input <input
v-model="connection.sshKeepAliveInterval" ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.client') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseSelect
v-model="connection.client"
:options="clients"
option-track-by="slug"
option-label="name"
class="form-select"
/>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionString') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="pgString"
v-model="connection.connString"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.databasePath"
:message="t('general.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.port"
class="form-input" class="form-input"
type="number" type="number"
min="1" min="1"
max="65535"
> >
<span class="input-group-addon">{{ t('general.seconds') }}</span>
</div> </div>
</div> </div>
<div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
:placeholder="clientCustomizations.defaultDatabase"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.schema') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.schema"
class="form-input"
type="text"
:placeholder="t('general.all')"
>
</div>
</div>
<div v-if="clientCustomizations.readOnlyMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }}
</label>
</div>
</div>
<div v-if="clientCustomizations.singleConnectionMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.singleConnectionMode" type="checkbox"><i class="form-icon" /> {{ t('connection.singleConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsl') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="connection.ssl">
<i class="form-icon" />
</label>
</div>
</div> </div>
</fieldset> <fieldset class="m-0" :disabled="isBusy || !connection.ssl">
</form> <div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.key"
:message="t('general.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.certificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.cert"
:message="t('general.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.caCertificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.ca"
:message="t('general.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.ciphers') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('connection.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsh') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="connection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.sshKey"
:message="t('general.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.passphrase') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPassphrase"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.keepAliveInterval') }}</label>
</div>
<div class="column col-7 col-sm-12">
<div class="input-group">
<input
v-model="connection.sshKeepAliveInterval"
class="form-input"
type="number"
min="1"
>
<span class="input-group-addon">{{ t('general.seconds') }}</span>
</div>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="panel-footer">
<div
@mouseenter="setCancelTestButtonVisibility(true)"
@mouseleave="setCancelTestButtonVisibility(false)"
>
<button
v-if="showTestCancel && isTesting"
class="btn btn-gray mr-2 cancellable"
:title="t('general.cancel')"
@click="abortConnection()"
>
<BaseIcon icon-name="mdiWindowClose" :size="24" />
<span class="d-invisible pr-1">{{ t('connection.testConnection') }}</span>
</button>
<button
v-else
id="connection-test"
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@click="startTest"
>
<BaseIcon
icon-name="mdiLightningBolt"
:size="24"
class="mr-1"
/>
{{ t('connection.testConnection') }}
</button>
</div>
<button
id="connection-save"
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy"
@click="saveConnection"
>
<BaseIcon
icon-name="mdiContentSave"
:size="24"
class="mr-1"
/>
{{ t('general.save') }}
</button>
</div> </div>
</div> </div>
<div class="panel-footer"> <ModalAskCredentials
<button v-if="isAsking"
id="connection-test" @close-asking="closeAsking"
class="btn btn-gray mr-2 d-flex" @credentials="continueTest"
:class="{'loading': isTesting}" />
:disabled="isBusy"
@click="startTest"
>
<BaseIcon
icon-name="mdiLightningBolt"
:size="24"
class="mr-1"
/>
{{ t('connection.testConnection') }}
</button>
<button
id="connection-save"
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy"
@click="saveConnection"
>
<BaseIcon
icon-name="mdiContentSave"
:size="24"
class="mr-1"
/>
{{ t('general.save') }}
</button>
</div>
</div> </div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div> </div>
<DebugConsole v-if="isConsoleOpen" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import customizations from 'common/customizations'; import customizations from 'common/customizations';
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { storeToRefs } from 'pinia';
import { computed, Ref, ref, watch } from 'vue'; import { computed, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import BaseUploadInput from '@/components/BaseUploadInput.vue'; import BaseUploadInput from '@/components/BaseUploadInput.vue';
import DebugConsole from '@/components/DebugConsole.vue';
import ModalAskCredentials from '@/components/ModalAskCredentials.vue'; import ModalAskCredentials from '@/components/ModalAskCredentials.vue';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -435,6 +464,7 @@ const { t } = useI18n();
const { addConnection } = useConnectionsStore(); const { addConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { isConsoleOpen } = storeToRefs(useConsoleStore());
const { connectWorkspace, selectWorkspace } = workspacesStore; const { connectWorkspace, selectWorkspace } = workspacesStore;
@@ -472,13 +502,15 @@ const connection = ref({
sshKey: '', sshKey: '',
sshPort: 22, sshPort: 22,
sshKeepAliveInterval: 1800, sshKeepAliveInterval: 1800,
pgConnString: '' connString: ''
}) as Ref<ConnectionParams & { pgConnString: string }>; }) as Ref<ConnectionParams & { connString: string }>;
const firstInput: Ref<HTMLInputElement> = ref(null); const firstInput: Ref<HTMLInputElement> = ref(null);
const isConnecting = ref(false); const isConnecting = ref(false);
const isTesting = ref(false); const isTesting = ref(false);
const isAsking = ref(false); const isAsking = ref(false);
const showTestCancel = ref(false);
const abortController: Ref<AbortController> = ref(new AbortController());
const selectedTab = ref('general'); const selectedTab = ref('general');
const clientCustomizations = computed(() => { const clientCustomizations = computed(() => {
@@ -501,6 +533,10 @@ const setDefaults = () => {
connection.value.database = clientCustomizations.value.defaultDatabase; connection.value.database = clientCustomizations.value.defaultDatabase;
}; };
const setCancelTestButtonVisibility = (val: boolean) => {
showTestCancel.value = val;
};
const startTest = async () => { const startTest = async () => {
isTesting.value = true; isTesting.value = true;
@@ -511,7 +547,7 @@ const startTest = async () => {
const res = await Connection.makeTest(connection.value); const res = await Connection.makeTest(connection.value);
if (res.status === 'error') if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() }); addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else else if (res.status === 'success')
addNotification({ status: 'success', message: t('connection.connectionSuccessfullyMade') }); addNotification({ status: 'success', message: t('connection.connectionSuccessfullyMade') });
} }
catch (err) { catch (err) {
@@ -522,13 +558,21 @@ const startTest = async () => {
} }
}; };
const abortConnection = (): void => {
abortController.value.abort();
Connection.abortConnection(connection.value.uid);
isTesting.value = false;
isConnecting.value = false;
abortController.value = new AbortController();
};
const continueTest = async (credentials: { user: string; password: string }) => { // if "Ask for credentials" is true const continueTest = async (credentials: { user: string; password: string }) => { // if "Ask for credentials" is true
isAsking.value = false; isAsking.value = false;
const params = Object.assign({}, connection.value, credentials); const params = Object.assign({}, connection.value, credentials);
try { try {
if (isConnecting.value) { if (isConnecting.value) {
await connectWorkspace(params); await connectWorkspace(params, { signal: abortController.value.signal }).catch(() => undefined);
isConnecting.value = false; isConnecting.value = false;
} }
else { else {
@@ -572,11 +616,11 @@ const pathSelection = (event: Event & {target: {files: {path: string}[]}}, name:
const { files } = event.target; const { files } = event.target;
if (!files.length) return; if (!files.length) return;
(connection.value as unknown as {[key: string]: string})[name] = files[0].path as string; (connection.value as unknown as Record<string, string>)[name] = files[0].path as string;
}; };
const pathClear = (name: keyof ConnectionParams) => { const pathClear = (name: keyof ConnectionParams) => {
(connection.value as unknown as {[key: string]: string})[name] = ''; (connection.value as unknown as Record<string, string>)[name] = '';
}; };
setDefaults(); setDefaults();

View File

@@ -68,7 +68,7 @@
<div class="column col-7 col-sm-12"> <div class="column col-7 col-sm-12">
<input <input
ref="pgString" ref="pgString"
v-model="localConnection.pgConnString" v-model="localConnection.connString"
class="form-input" class="form-input"
type="text" type="text"
> >
@@ -165,22 +165,30 @@
> >
</div> </div>
</div> </div>
<div v-if="clientCustomizations.readOnlyMode" class="form-group columns"> <div v-if="clientCustomizations.readOnlyMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" /> <div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12"> <div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline my-0">
<input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }} <input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" /> <div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12"> <div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline my-0">
<input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }} <input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="clientCustomizations.singleConnectionMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="localConnection.singleConnectionMode" type="checkbox"><i class="form-icon" /> {{ t('connection.singleConnection') }}
</label>
</div>
</div>
</fieldset> </fieldset>
</form> </form>
</div> </div>
@@ -379,20 +387,35 @@
</div> </div>
</div> </div>
<div class="panel-footer"> <div class="panel-footer">
<button <div
id="connection-test" @mouseenter="setCancelTestButtonVisibility(true)"
class="btn btn-gray mr-2 d-flex" @mouseleave="setCancelTestButtonVisibility(false)"
:class="{'loading': isTesting}"
:disabled="isBusy"
@click="startTest"
> >
<BaseIcon <button
icon-name="mdiLightningBolt" v-if="showTestCancel && isTesting"
:size="24" class="btn btn-gray mr-2 cancellable"
class="mr-1" :title="t('general.cancel')"
/> @click="abortConnection()"
{{ t('connection.testConnection') }} >
</button> <BaseIcon icon-name="mdiWindowClose" :size="24" />
<span class="d-invisible pr-1">{{ t('connection.testConnection') }}</span>
</button>
<button
v-else
id="connection-test"
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@click="startTest"
>
<BaseIcon
icon-name="mdiLightningBolt"
:size="24"
class="mr-1"
/>
{{ t('connection.testConnection') }}
</button>
</div>
<button <button
id="connection-save" id="connection-save"
class="btn btn-primary mr-2 d-flex" class="btn btn-primary mr-2 d-flex"
@@ -406,20 +429,35 @@
/> />
{{ t('general.save') }} {{ t('general.save') }}
</button> </button>
<button <div
id="connection-connect" @mouseenter="setCancelConnectButtonVisibility(true)"
class="btn btn-success d-flex" @mouseleave="setCancelConnectButtonVisibility(false)"
:class="{'loading': isConnecting}"
:disabled="isBusy"
@click="startConnection"
> >
<BaseIcon <button
icon-name="mdiConnection" v-if="showConnectCancel && isConnecting"
:size="24" class="btn btn-success cancellable"
class="mr-1" :title="t('general.cancel')"
/> @click="abortConnection()"
{{ t('connection.connect') }} >
</button> <BaseIcon icon-name="mdiWindowClose" :size="24" />
<span class="d-invisible pr-1">{{ t('connection.connect') }}</span>
</button>
<button
v-else
id="connection-connect"
class="btn btn-success d-flex"
:class="{'loading': isConnecting}"
:disabled="isBusy"
@click="startConnection"
>
<BaseIcon
icon-name="mdiConnection"
:size="24"
class="mr-1"
/>
{{ t('connection.connect') }}
</button>
</div>
</div> </div>
</div> </div>
<ModalAskCredentials <ModalAskCredentials
@@ -464,10 +502,13 @@ const clients = [
]; ];
const firstInput: Ref<HTMLInputElement> = ref(null); const firstInput: Ref<HTMLInputElement> = ref(null);
const localConnection: Ref<ConnectionParams & { pgConnString: string }> = ref(null); const localConnection: Ref<ConnectionParams & { connString: string }> = ref(null);
const isConnecting = ref(false); const isConnecting = ref(false);
const isTesting = ref(false); const isTesting = ref(false);
const isAsking = ref(false); const isAsking = ref(false);
const showTestCancel = ref(false);
const showConnectCancel = ref(false);
const abortController: Ref<AbortController> = ref(new AbortController());
const selectedTab = ref('general'); const selectedTab = ref('general');
const clientCustomizations = computed(() => { const clientCustomizations = computed(() => {
@@ -493,7 +534,7 @@ const startConnection = async () => {
if (localConnection.value.ask) if (localConnection.value.ask)
isAsking.value = true; isAsking.value = true;
else { else {
await connectWorkspace(localConnection.value); await connectWorkspace(localConnection.value, { signal: abortController.value.signal }).catch(() => undefined);
isConnecting.value = false; isConnecting.value = false;
} }
}; };
@@ -508,7 +549,7 @@ const startTest = async () => {
const res = await Connection.makeTest(localConnection.value); const res = await Connection.makeTest(localConnection.value);
if (res.status === 'error') if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() }); addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else else if (res.status === 'success')
addNotification({ status: 'success', message: t('connection.connectionSuccessfullyMade') }); addNotification({ status: 'success', message: t('connection.connectionSuccessfullyMade') });
} }
catch (err) { catch (err) {
@@ -519,20 +560,36 @@ const startTest = async () => {
} }
}; };
const setCancelTestButtonVisibility = (val: boolean) => {
showTestCancel.value = val;
};
const setCancelConnectButtonVisibility = (val: boolean) => {
showConnectCancel.value = val;
};
const abortConnection = (): void => {
abortController.value.abort();
Connection.abortConnection(localConnection.value.uid);
isTesting.value = false;
isConnecting.value = false;
abortController.value = new AbortController();
};
const continueTest = async (credentials: {user: string; password: string }) => { // if "Ask for credentials" is true const continueTest = async (credentials: {user: string; password: string }) => { // if "Ask for credentials" is true
isAsking.value = false; isAsking.value = false;
const params = Object.assign({}, localConnection.value, credentials); const params = Object.assign({}, localConnection.value, credentials);
try { try {
if (isConnecting.value) { if (isConnecting.value) {
const params = Object.assign({}, props.connection, credentials); const params = Object.assign({}, props.connection, credentials);
await connectWorkspace(params); await connectWorkspace(params, { signal: abortController.value.signal }).catch(() => undefined);
isConnecting.value = false; isConnecting.value = false;
} }
else { else {
const res = await Connection.makeTest(params); const res = await Connection.makeTest(params);
if (res.status === 'error') if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() }); addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else else if (res.status === 'success')
addNotification({ status: 'success', message: t('connection.connectionSuccessfullyMade') }); addNotification({ status: 'success', message: t('connection.connectionSuccessfullyMade') });
} }
} }
@@ -569,11 +626,11 @@ const pathSelection = (event: Event & {target: {files: {path: string}[]}}, name:
const { files } = event.target; const { files } = event.target;
if (!files.length) return; if (!files.length) return;
(localConnection.value as unknown as {[key: string]: string})[name] = files[0].path; (localConnection.value as unknown as Record<string, string>)[name] = files[0].path;
}; };
const pathClear = (name: keyof ConnectionParams) => { const pathClear = (name: keyof ConnectionParams) => {
(localConnection.value as unknown as {[key: string]: string})[name] = ''; (localConnection.value as unknown as Record<string, string>)[name] = '';
}; };
localConnection.value = JSON.parse(JSON.stringify(props.connection)); localConnection.value = JSON.parse(JSON.stringify(props.connection));
@@ -590,6 +647,22 @@ localConnection.value = JSON.parse(JSON.stringify(props.connection));
min-width: 450px; min-width: 450px;
border-radius: $border-radius; border-radius: $border-radius;
.panel-nav {
.tab-block {
background: transparent;
margin: 0.2rem 0 0.15rem 0;
.tab-item {
background: transparent;
flex: 1 0 0;
> a {
padding: 8px 4px 6px 4px
}
}
}
}
.panel-body { .panel-body {
flex: initial; flex: initial;
} }

View File

@@ -19,6 +19,8 @@
v-model="selectedDatabase" v-model="selectedDatabase"
:options="databases" :options="databases"
class="form-select select-sm text-bold my-0" class="form-select select-sm text-bold my-0"
@keypress.stop=""
@keydown.stop=""
/> />
</div> </div>
<span v-else class="workspace-explorebar-title">{{ connectionName }}</span> <span v-else class="workspace-explorebar-title">{{ connectionName }}</span>
@@ -109,6 +111,7 @@
@close-context="closeDatabaseContext" @close-context="closeDatabaseContext"
@open-create-table-tab="openCreateElementTab('table')" @open-create-table-tab="openCreateElementTab('table')"
@open-create-view-tab="openCreateElementTab('view')" @open-create-view-tab="openCreateElementTab('view')"
@open-create-materialized-view-tab="openCreateElementTab('materialized-view')"
@open-create-trigger-tab="openCreateElementTab('trigger')" @open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-routine-tab="openCreateElementTab('routine')" @open-create-routine-tab="openCreateElementTab('routine')"
@open-create-function-tab="openCreateElementTab('function')" @open-create-function-tab="openCreateElementTab('function')"
@@ -139,10 +142,12 @@
:selected-misc="selectedMisc" :selected-misc="selectedMisc"
:selected-schema="selectedSchema" :selected-schema="selectedSchema"
:context-event="miscContextEvent" :context-event="miscContextEvent"
@open-create-view-tab="openCreateElementTab('view')"
@open-create-materializedView-tab="openCreateElementTab('materialized-view')"
@open-create-trigger-tab="openCreateElementTab('trigger')" @open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-routine-tab="openCreateElementTab('routine')" @open-create-routine-tab="openCreateElementTab('routine')"
@open-create-function-tab="openCreateElementTab('function')" @open-create-function-tab="openCreateElementTab('function')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-scheduler-tab="openCreateElementTab('scheduler')" @open-create-scheduler-tab="openCreateElementTab('scheduler')"
@close-context="closeMiscFolderContext" @close-context="closeMiscFolderContext"
@reload="refresh" @reload="refresh"
@@ -501,7 +506,7 @@ const toggleSearchMethod = () => {
transition: background 0.2s; transition: background 0.2s;
&:hover { &:hover {
background: rgba($primary-color, 50%); background: var(--primary-color-dark);
} }
} }

View File

@@ -16,7 +16,7 @@
/> {{ t('general.run') }}</span> /> {{ t('general.run') }}</span>
</div> </div>
<div <div
v-if="selectedMisc.type === 'trigger' && customizations.triggerEnableDisable" v-if="selectedMisc.type === 'trigger' && customizations.triggerEnableDisable && !connection.readonly"
class="context-element" class="context-element"
@click="toggleTrigger" @click="toggleTrigger"
> >
@@ -36,7 +36,7 @@
</span> </span>
</div> </div>
<div <div
v-if="selectedMisc.type === 'scheduler'" v-if="selectedMisc.type === 'scheduler' && !connection.readonly"
class="context-element" class="context-element"
@click="toggleScheduler" @click="toggleScheduler"
> >
@@ -63,7 +63,11 @@
:size="18" :size="18"
/> {{ t('general.copyName') }}</span> /> {{ t('general.copyName') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div
v-if="!connection.readonly"
class="context-element"
@click="showDeleteModal"
>
<span class="d-flex"> <span class="d-flex">
<BaseIcon <BaseIcon
class="text-light mt-1 mr-1" class="text-light mt-1 mr-1"
@@ -117,6 +121,7 @@ import Routines from '@/ipc-api/Routines';
import Schedulers from '@/ipc-api/Schedulers'; import Schedulers from '@/ipc-api/Schedulers';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
import { copyText } from '@/libs/copyText'; import { copyText } from '@/libs/copyText';
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';
@@ -132,6 +137,7 @@ const emit = defineEmits(['close-context', 'reload']);
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { getConnectionByUid } = useConnectionsStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
@@ -154,6 +160,7 @@ const workspace = computed(() => {
const customizations = computed(() => { const customizations = computed(() => {
return getWorkspace(selectedWorkspace.value).customizations; return getWorkspace(selectedWorkspace.value).customizations;
}); });
const connection = computed(() => getConnectionByUid(selectedWorkspace.value));
const deleteMessage = computed(() => { const deleteMessage = computed(() => {
switch (props.selectedMisc.type) { switch (props.selectedMisc.type) {

View File

@@ -3,6 +3,30 @@
:context-event="props.contextEvent" :context-event="props.contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div
v-if="props.selectedMisc === 'view'"
class="context-element"
@click="emit('open-create-view-tab')"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiTableCog"
:size="18"
/> {{ t('database.createNewView') }}</span>
</div>
<div
v-if="props.selectedMisc === 'materializedView'"
class="context-element"
@click="emit('open-create-materializedView-tab')"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiTableCog"
:size="18"
/> {{ t('database.createNewMaterializedView') }}</span>
</div>
<div <div
v-if="props.selectedMisc === 'trigger'" v-if="props.selectedMisc === 'trigger'"
class="context-element" class="context-element"
@@ -81,6 +105,8 @@ const props = defineProps({
}); });
const emit = defineEmits([ const emit = defineEmits([
'open-create-view-tab',
'open-create-materializedView-tab',
'open-create-trigger-tab', 'open-create-trigger-tab',
'open-create-routine-tab', 'open-create-routine-tab',
'open-create-function-tab', 'open-create-function-tab',

View File

@@ -67,6 +67,104 @@
</ul> </ul>
</div> </div>
<div v-if="filteredViews.length" class="database-misc">
<details class="accordion">
<summary
class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}"
@contextmenu.prevent="showMiscFolderContext($event, 'view')"
>
<BaseIcon
class="misc-icon mr-1"
icon-name="mdiFolderEye"
:size="18"
/>
<BaseIcon
class="misc-icon open-folder mr-1"
icon-name="mdiFolderOpen"
:size="18"
/>
{{ t('database.view', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="view of filteredViews"
:key="view.name"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.view === view.name}"
@mousedown.left="selectTable({schema: database.name, table: view})"
@dblclick="openDataTab({schema: database.name, table: view})"
@contextmenu.prevent="showTableContext($event, view)"
>
<a class="table-name">
<div v-if="checkLoadingStatus(view.name, 'table')" class="icon loading mr-1" />
<BaseIcon
v-else
class="table-icon mr-1"
icon-name="mdiTableEye"
:size="18"
:style="`min-width: 18px`"
/>
<span v-html="highlightWord(view.name)" />
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="filteredMatViews.length && customizations.materializedViews" class="database-misc">
<details class="accordion">
<summary
class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}"
@contextmenu.prevent="showMiscFolderContext($event, 'materializedView')"
>
<BaseIcon
class="misc-icon mr-1"
icon-name="mdiFolderEye"
:size="18"
/>
<BaseIcon
class="misc-icon open-folder mr-1"
icon-name="mdiFolderOpen"
:size="18"
/>
{{ t('database.materializedView', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="view of filteredMatViews"
:key="view.name"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.view === view.name}"
@mousedown.left="selectTable({schema: database.name, table: view})"
@dblclick="openDataTab({schema: database.name, table: view})"
@contextmenu.prevent="showTableContext($event, view)"
>
<a class="table-name">
<div v-if="checkLoadingStatus(view.name, 'table')" class="icon loading mr-1" />
<BaseIcon
v-else
class="table-icon mr-1"
icon-name="mdiTableEye"
:size="18"
:style="`min-width: 18px`"
/>
<span v-html="highlightWord(view.name)" />
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="filteredTriggers.length && customizations.triggers" class="database-misc"> <div v-if="filteredTriggers.length && customizations.triggers" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary <summary
@@ -379,12 +477,30 @@ const searchTerm = computed(() => {
}); });
const filteredTables = computed(() => { const filteredTables = computed(() => {
if (props.searchMethod === 'elements') if (props.searchMethod === 'elements') {
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0); const searchTermLower = searchTerm.value.toLowerCase();
return props.database.tables.filter(table =>
table.name.toLowerCase().includes(searchTermLower) && table.type === 'table'
);
}
else else
return props.database.tables; return props.database.tables;
}); });
const filteredViews = computed(() => {
if (props.searchMethod === 'elements')
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && table.type === 'view');
else
return props.database.tables.filter(table => table.type === 'view');
});
const filteredMatViews = computed(() => {
if (props.searchMethod === 'elements')
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && table.type === 'materializedView');
else
return props.database.tables.filter(table => table.type === 'materializedView');
});
const filteredTriggers = computed(() => { const filteredTriggers = computed(() => {
if (props.searchMethod === 'elements') if (props.searchMethod === 'elements')
return props.database.triggers.filter(trigger => trigger.name.search(searchTerm.value) >= 0); return props.database.triggers.filter(trigger => trigger.name.search(searchTerm.value) >= 0);
@@ -513,7 +629,13 @@ const selectMisc = ({ schema, misc, type }: { schema: string; misc: { name: stri
}; };
const openDataTab = ({ schema, table }: { schema: string; table: TableInfos }) => { const openDataTab = ({ schema, table }: { schema: string; table: TableInfos }) => {
newTab({ uid: props.connection.uid, elementName: table.name, schema: props.database.name, type: 'data', elementType: table.type }); newTab({
uid: props.connection.uid,
elementName: table.name,
schema: props.database.name,
type: 'data',
elementType: table.type
});
setBreadcrumbs({ schema, [table.type]: table.name }); setBreadcrumbs({ schema, [table.type]: table.name });
}; };

View File

@@ -3,7 +3,7 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div class="context-element"> <div v-if="!connection.readonly" class="context-element">
<span class="d-flex"> <span class="d-flex">
<BaseIcon <BaseIcon
class="text-light mt-1 mr-1" class="text-light mt-1 mr-1"
@@ -40,6 +40,18 @@
:size="18" :size="18"
/> {{ t('database.view') }}</span> /> {{ t('database.view') }}</span>
</div> </div>
<div
v-if="workspace.customizations.materializedViewAdd"
class="context-element"
@click="openCreateMaterializedViewTab"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiTableEye"
:size="18"
/> {{ t('database.materializedView') }}</span>
</div>
<div <div
v-if="workspace.customizations.triggerAdd" v-if="workspace.customizations.triggerAdd"
class="context-element" class="context-element"
@@ -123,7 +135,7 @@
/> {{ t('database.export') }}</span> /> {{ t('database.export') }}</span>
</div> </div>
<div <div
v-if="workspace.customizations.schemaImport" v-if="workspace.customizations.schemaImport && !connection.readonly"
class="context-element" class="context-element"
@click="initImport" @click="initImport"
> >
@@ -135,7 +147,7 @@
/> {{ t('database.import') }}</span> /> {{ t('database.import') }}</span>
</div> </div>
<div <div
v-if="workspace.customizations.schemaEdit" v-if="workspace.customizations.schemaEdit && !connection.readonly"
class="context-element" class="context-element"
@click="showEditModal" @click="showEditModal"
> >
@@ -147,7 +159,7 @@
/> {{ t('database.editSchema') }}</span> /> {{ t('database.editSchema') }}</span>
</div> </div>
<div <div
v-if="workspace.customizations.schemaDrop" v-if="workspace.customizations.schemaDrop && !connection.readonly"
class="context-element" class="context-element"
@click="showDeleteModal" @click="showDeleteModal"
> >
@@ -207,6 +219,7 @@ import ModalImportSchema from '@/components/ModalImportSchema.vue';
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 { copyText } from '@/libs/copyText'; import { copyText } from '@/libs/copyText';
import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useSchemaExportStore } from '@/stores/schemaExport'; import { useSchemaExportStore } from '@/stores/schemaExport';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -221,6 +234,7 @@ const props = defineProps({
const emit = defineEmits([ const emit = defineEmits([
'open-create-table-tab', 'open-create-table-tab',
'open-create-view-tab', 'open-create-view-tab',
'open-create-materialized-view-tab',
'open-create-trigger-tab', 'open-create-trigger-tab',
'open-create-routine-tab', 'open-create-routine-tab',
'open-create-function-tab', 'open-create-function-tab',
@@ -235,7 +249,9 @@ const workspacesStore = useWorkspacesStore();
const schemaExportStore = useSchemaExportStore(); const schemaExportStore = useSchemaExportStore();
const { showExportModal } = schemaExportStore; const { showExportModal } = schemaExportStore;
const connectionsStore = useConnectionsStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getConnectionByUid } = connectionsStore;
const { const {
getWorkspace, getWorkspace,
@@ -248,6 +264,7 @@ const isEditModal = ref(false);
const isImportSchemaModal = ref(false); const isImportSchemaModal = ref(false);
const workspace = computed(() => getWorkspace(selectedWorkspace.value)); const workspace = computed(() => getWorkspace(selectedWorkspace.value));
const connection = computed(() => getConnectionByUid(selectedWorkspace.value));
const openCreateTableTab = () => { const openCreateTableTab = () => {
emit('open-create-table-tab'); emit('open-create-table-tab');
@@ -257,6 +274,10 @@ const openCreateViewTab = () => {
emit('open-create-view-tab'); emit('open-create-view-tab');
}; };
const openCreateMaterializedViewTab = () => {
emit('open-create-materialized-view-tab');
};
const openCreateTriggerTab = () => { const openCreateTriggerTab = () => {
emit('open-create-trigger-tab'); emit('open-create-trigger-tab');
}; };

View File

@@ -48,7 +48,19 @@
/> {{ t('application.settings') }}</span> /> {{ t('application.settings') }}</span>
</div> </div>
<div <div
v-if="selectedTable && selectedTable.type === 'table' && customizations.tableDuplicate" v-if="selectedTable && selectedTable.type === 'materializedView' && customizations.materializedViewSettings"
class="context-element"
@click="openMaterializedViewSettingTab"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiWrenchCog"
:size="18"
/> {{ t('application.settings') }}</span>
</div>
<div
v-if="selectedTable && selectedTable.type === 'table' && customizations.tableDuplicate && !connection.readonly"
class="context-element" class="context-element"
@click="duplicateTable" @click="duplicateTable"
> >
@@ -60,7 +72,7 @@
/> {{ t('database.duplicateTable') }}</span> /> {{ t('database.duplicateTable') }}</span>
</div> </div>
<div <div
v-if="selectedTable && selectedTable.type === 'table'" v-if="selectedTable && selectedTable.type === 'table' && !connection.readonly"
class="context-element" class="context-element"
@click="showEmptyModal" @click="showEmptyModal"
> >
@@ -71,7 +83,11 @@
:size="18" :size="18"
/> {{ t('database.emptyTable') }}</span> /> {{ t('database.emptyTable') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div
v-if="!connection.readonly"
class="context-element"
@click="showDeleteModal"
>
<span class="d-flex"> <span class="d-flex">
<BaseIcon <BaseIcon
class="text-light mt-1 mr-1" class="text-light mt-1 mr-1"
@@ -139,6 +155,7 @@ import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { copyText } from '@/libs/copyText'; import { copyText } from '@/libs/copyText';
import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useSchemaExportStore } from '@/stores/schemaExport'; import { useSchemaExportStore } from '@/stores/schemaExport';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@@ -156,6 +173,7 @@ const emit = defineEmits(['close-context', 'duplicate-table', 'reload', 'delete-
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { showExportModal } = useSchemaExportStore(); const { showExportModal } = useSchemaExportStore();
const { getConnectionByUid } = useConnectionsStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
@@ -173,6 +191,7 @@ const forceTruncate = ref(false);
const workspace = computed(() => getWorkspace(selectedWorkspace.value)); const workspace = computed(() => getWorkspace(selectedWorkspace.value));
const customizations = computed(() => workspace.value && workspace.value.customizations ? workspace.value.customizations : null); const customizations = computed(() => workspace.value && workspace.value.customizations ? workspace.value.customizations : null);
const connection = computed(() => getConnectionByUid(selectedWorkspace.value));
const showTableExportModal = () => { const showTableExportModal = () => {
showExportModal(props.selectedSchema, props.selectedTable.name); showExportModal(props.selectedSchema, props.selectedTable.name);
@@ -238,6 +257,23 @@ const openViewSettingTab = () => {
closeContext(); closeContext();
}; };
const openMaterializedViewSettingTab = () => {
newTab({
uid: selectedWorkspace.value,
elementType: 'table',
elementName: props.selectedTable.name,
schema: props.selectedSchema,
type: 'materialized-view-props'
});
changeBreadcrumbs({
schema: props.selectedSchema,
view: props.selectedTable.name
});
closeContext();
};
const duplicateTable = () => { const duplicateTable = () => {
emit('duplicate-table', { schema: props.selectedSchema, table: props.selectedTable }); emit('duplicate-table', { schema: props.selectedSchema, table: props.selectedTable });
}; };

View File

@@ -1,189 +0,0 @@
<template>
<div
ref="wrapper"
class="query-console-wrapper"
@mouseenter="isHover = true"
@mouseleave="isHover = false"
>
<div ref="resizer" class="query-console-resizer" />
<div
id="query-console"
ref="queryConsole"
class="query-console column col-12"
:style="{height: localHeight ? localHeight+'px' : ''}"
>
<div class="query-console-header">
<div>{{ t('application.console') }}</div>
<button class="btn btn-clear mr-1" @click="resizeConsole(0)" />
</div>
<div ref="queryConsoleBody" class="query-console-body">
<div
v-for="(wLog, i) in workspaceLogs"
:key="i"
class="query-console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)"
>
<span class="type-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="query-console-log-sql" v-html="highlight(wLog.sql, {html: true})" />
</div>
</div>
</div>
</div>
<BaseContextMenu
v-if="isContext"
:context-event="contextEvent"
@close-context="isContext = false"
>
<div class="context-element" @click="copyQuery">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copy') }}</span>
</div>
</BaseContextMenu>
</template>
<script setup lang="ts">
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { highlight } from 'sql-highlight';
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import { copyText } from '@/libs/copyText';
import { useConsoleStore } from '@/stores/console';
const { t } = useI18n();
const consoleStore = useConsoleStore();
const { resizeConsole, getLogsByWorkspace } = consoleStore;
const { consoleHeight } = storeToRefs(consoleStore);
const props = defineProps({
uid: String
});
const wrapper: Ref<HTMLInputElement> = ref(null);
const queryConsole: Ref<HTMLInputElement> = ref(null);
const queryConsoleBody: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null);
const localHeight = ref(250);
const isHover = ref(false);
const isContext = ref(false);
const contextQuery: Ref<string> = ref(null);
const contextEvent: Ref<MouseEvent> = ref(null);
const resize = (e: MouseEvent) => {
const el = queryConsole.value;
let consoleHeight = el.getBoundingClientRect().bottom - e.pageY;
if (consoleHeight > 400) consoleHeight = 400;
localHeight.value = consoleHeight;
};
const workspaceLogs = computed(() => {
return getLogsByWorkspace(props.uid);
});
const stopResize = () => {
if (localHeight.value < 0) localHeight.value = 0;
resizeConsole(localHeight.value);
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResize);
};
const contextMenu = (event: MouseEvent, wLog: {date: Date; sql: string}) => {
contextEvent.value = event;
contextQuery.value = wLog.sql;
isContext.value = true;
};
const copyQuery = () => {
copyText(contextQuery.value);
isContext.value = false;
};
watch(workspaceLogs, async () => {
if (!isHover.value) {
await nextTick();
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
}
});
onMounted(() => {
localHeight.value = consoleHeight.value;
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
});
onMounted(() => {
resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
});
</script>
<style lang="scss" scoped>
.query-console-wrapper {
width: 100%;
z-index: 9;
margin-top: auto;
position: absolute;
bottom: 0;
.query-console-resizer {
height: 4px;
top: -1px;
width: 100%;
cursor: ns-resize;
position: absolute;
z-index: 99;
transition: background 0.2s;
&:hover {
background: rgba($primary-color, 50%);
}
}
.query-console {
padding: 0;
padding-bottom: $footer-height;
.query-console-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px;
font-weight: 700;
}
.query-console-body {
overflow: auto;
display: flex;
flex-direction: column;
max-height: 100%;
padding: 0 6px 3px;
.query-console-log {
padding: 1px 3px;
margin: 1px 0;
border-radius: $border-radius;
.query-console-log-sql {
font-size: 95%;
opacity: 0.8;
font-weight: 700;
&:hover {
user-select: text;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,293 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<BaseIcon
class="mr-1"
icon-name="mdiContentSave"
:size="24"
/>
<span>{{ t('general.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="t('database.clearChanges')"
@click="clearChanges"
>
<BaseIcon
class="mr-1"
icon-name="mdiDeleteSweep"
:size="24"
/>
<span>{{ t('general.clear') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="t('database.schema')">
<BaseIcon
class="mt-1 mr-1"
icon-name="mdiDatabase"
:size="18"
/><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ t('general.name') }}</label>
<input
ref="firstInput"
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ t('database.definer') }}</label>
<BaseSelect
v-model="localView.definer"
:options="users"
:option-label="(user: any) => user.value === '' ? t('database.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ t('database.sqlSecurity') }}</label>
<BaseSelect
v-model="localView.security"
:options="['DEFINER', 'INVOKER']"
class="form-select"
/>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ t('database.algorithm') }}</label>
<BaseSelect
v-model="localView.algorithm"
:options="['UNDEFINED', 'MERGE', 'TEMPTABLE']"
class="form-select"
/>
</div>
</div>
<div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ t('database.updateOption') }}</label>
<BaseSelect
v-model="localView.updateOption"
:option-track-by="(user: any) => user.value"
:options="[{label: 'None', value: ''}, {label: 'CASCADED', value: 'CASCADED'}, {label: 'LOCAL', value: 'LOCAL'}]"
class="form-select"
/>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ t('database.selectStatement') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
v-model="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { Ace } from 'ace-builds';
import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia';
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseLoader from '@/components/BaseLoader.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import Views from '@/ipc-api/Views';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
const { t } = useI18n();
const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
refreshStructure,
setUnsavedChanges,
changeBreadcrumbs,
newTab,
removeTab
} = workspacesStore;
const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
const firstInput: Ref<HTMLInputElement> = ref(null);
const isLoading = ref(false);
const isSaving = ref(false);
const originalView = ref(null);
const localView = ref(null);
const editorHeight = ref(300);
const workspace = computed(() => getWorkspace(props.connection.uid));
const isChanged = computed(() => JSON.stringify(originalView.value) !== JSON.stringify(localView.value));
const isDefinerInUsers = computed(() => originalView.value ? workspace.value.users.some(user => originalView.value.definer === `\`${user.name}\`@\`${user.host}\``) : true);
const users = computed(() => {
const users = [{ value: '' }, ...workspace.value.users];
if (!isDefinerInUsers.value) {
const [name, host] = originalView.value.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
return users;
});
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
schema: props.schema,
...localView.value
};
try {
const { status, response } = await Views.createMaterializedView(params);
if (status === 'success') {
await refreshStructure(props.connection.uid);
newTab({
uid: props.connection.uid,
schema: props.schema,
elementName: localView.value.name,
elementType: 'materializedView',
type: 'materialized-view-props'
});
removeTab({ uid: props.connection.uid, tab: props.tab.uid });
changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isSaving.value = false;
};
const clearChanges = () => {
localView.value = JSON.parse(JSON.stringify(originalView.value));
queryEditor.value.editor.session.setValue(localView.value.sql);
};
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
const saveContentListener = () => {
const hasModalOpen = !!document.querySelectorAll('.modal.active').length;
if (props.isSelected && !hasModalOpen && isChanged.value)
saveChanges();
};
watch(() => props.isSelected, (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
setTimeout(() => {
resizeQueryEditor();
}, 50);
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
originalView.value = {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
};
localView.value = JSON.parse(JSON.stringify(originalView.value));
setTimeout(() => {
resizeQueryEditor();
}, 50);
onMounted(() => {
if (props.isSelected)
changeBreadcrumbs({ schema: props.schema });
ipcRenderer.on('save-content', saveContentListener);
setTimeout(() => {
firstInput.value.focus();
}, 100);
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
ipcRenderer.removeListener('save-content', saveContentListener);
});
</script>

View File

@@ -72,6 +72,20 @@
/> />
<span>{{ t('database.foreignKeys') }}</span> <span>{{ t('database.foreignKeys') }}</span>
</button> </button>
<button
v-if="workspace.customizations.tableCheck"
class="btn btn-dark btn-sm ml-2 mr-0"
:disabled="isSaving || !localFields.length"
:title="t('database.manageTableChecks')"
@click="showTableChecksModal"
>
<BaseIcon
class="mr-1"
icon-name="mdiTableCheck"
:size="24"
/>
<span>{{ t('database.tableChecks') }}</span>
</button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="t('database.schema')"> <div class="d-flex" :title="t('database.schema')">
@@ -183,11 +197,19 @@
@hide="hideForeignModal" @hide="hideForeignModal"
@foreigns-update="foreignsUpdate" @foreigns-update="foreignsUpdate"
/> />
<WorkspaceTabPropsTableChecksModal
v-if="isTableChecksModal"
:local-checks="localTableChecks"
table="new"
:workspace="workspace"
@hide="hideTableChecksModal"
@checks-update="checksUpdate"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ConnectionParams, TableField, TableForeign, TableIndex, TableOptions } from 'common/interfaces/antares'; import { ConnectionParams, TableCheck, TableField, TableForeign, TableIndex, TableOptions } from 'common/interfaces/antares';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@@ -198,6 +220,7 @@ import BaseIcon from '@/components/BaseIcon.vue';
import BaseLoader from '@/components/BaseLoader.vue'; import BaseLoader from '@/components/BaseLoader.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import WorkspaceTabNewTableEmptyState from '@/components/WorkspaceTabNewTableEmptyState.vue'; import WorkspaceTabNewTableEmptyState from '@/components/WorkspaceTabNewTableEmptyState.vue';
import WorkspaceTabPropsTableChecksModal from '@/components/WorkspaceTabPropsTableChecksModal.vue';
import WorkspaceTabPropsTableFields from '@/components/WorkspaceTabPropsTableFields.vue'; import WorkspaceTabPropsTableFields from '@/components/WorkspaceTabPropsTableFields.vue';
import WorkspaceTabPropsTableForeignModal from '@/components/WorkspaceTabPropsTableForeignModal.vue'; import WorkspaceTabPropsTableForeignModal from '@/components/WorkspaceTabPropsTableForeignModal.vue';
import WorkspaceTabPropsTableIndexesModal from '@/components/WorkspaceTabPropsTableIndexesModal.vue'; import WorkspaceTabPropsTableIndexesModal from '@/components/WorkspaceTabPropsTableIndexesModal.vue';
@@ -236,12 +259,16 @@ const isLoading = ref(false);
const isSaving = ref(false); const isSaving = ref(false);
const isIndexesModal = ref(false); const isIndexesModal = ref(false);
const isForeignModal = ref(false); const isForeignModal = ref(false);
const isTableChecksModal = ref(false);
const originalFields: Ref<TableField[]> = ref([]); const originalFields: Ref<TableField[]> = ref([]);
const localFields: Ref<TableField[]> = ref([]); const localFields: Ref<TableField[]> = ref([]);
const originalKeyUsage: Ref<TableForeign[]> = ref([]); const originalKeyUsage: Ref<TableForeign[]> = ref([]);
const localKeyUsage: Ref<TableForeign[]> = ref([]); const localKeyUsage: Ref<TableForeign[]> = ref([]);
const originalIndexes: Ref<TableIndex[]> = ref([]); const originalIndexes: Ref<TableIndex[]> = ref([]);
const localIndexes: Ref<TableIndex[]> = ref([]); const localIndexes: Ref<TableIndex[]> = ref([]);
const originalTableChecks: Ref<TableCheck[]> = ref([]);
const localTableChecks: Ref<TableCheck[]> = ref([]);
const tableOptions: Ref<TableOptions> = ref(null); const tableOptions: Ref<TableOptions> = ref(null);
const localOptions: Ref<TableOptions> = ref(null); const localOptions: Ref<TableOptions> = ref(null);
const newFieldsCounter = ref(0); const newFieldsCounter = ref(0);
@@ -274,6 +301,7 @@ const isChanged = computed(() => {
return JSON.stringify(originalFields.value) !== JSON.stringify(localFields.value) || return JSON.stringify(originalFields.value) !== JSON.stringify(localFields.value) ||
JSON.stringify(originalKeyUsage.value) !== JSON.stringify(localKeyUsage.value) || JSON.stringify(originalKeyUsage.value) !== JSON.stringify(localKeyUsage.value) ||
JSON.stringify(originalIndexes.value) !== JSON.stringify(localIndexes.value) || JSON.stringify(originalIndexes.value) !== JSON.stringify(localIndexes.value) ||
JSON.stringify(originalTableChecks.value) !== JSON.stringify(localTableChecks.value) ||
JSON.stringify(tableOptions.value) !== JSON.stringify(localOptions.value); JSON.stringify(tableOptions.value) !== JSON.stringify(localOptions.value);
}); });
@@ -291,6 +319,7 @@ const saveChanges = async () => {
fields: localFields.value, fields: localFields.value,
foreigns: localKeyUsage.value, foreigns: localKeyUsage.value,
indexes: localIndexes.value, indexes: localIndexes.value,
checks: localTableChecks.value,
options: localOptions.value options: localOptions.value
}; };
@@ -326,6 +355,7 @@ const clearChanges = () => {
localFields.value = JSON.parse(JSON.stringify(originalFields.value)); localFields.value = JSON.parse(JSON.stringify(originalFields.value));
localIndexes.value = JSON.parse(JSON.stringify(originalIndexes.value)); localIndexes.value = JSON.parse(JSON.stringify(originalIndexes.value));
localKeyUsage.value = JSON.parse(JSON.stringify(originalKeyUsage.value)); localKeyUsage.value = JSON.parse(JSON.stringify(originalKeyUsage.value));
localTableChecks.value = JSON.parse(JSON.stringify(originalTableChecks.value));
tableOptions.value = { tableOptions.value = {
name: '', name: '',
@@ -446,10 +476,22 @@ const hideForeignModal = () => {
isForeignModal.value = false; isForeignModal.value = false;
}; };
const showTableChecksModal = () => {
isTableChecksModal.value = true;
};
const hideTableChecksModal = () => {
isTableChecksModal.value = false;
};
const foreignsUpdate = (foreigns: TableForeign[]) => { const foreignsUpdate = (foreigns: TableForeign[]) => {
localKeyUsage.value = foreigns; localKeyUsage.value = foreigns;
}; };
const checksUpdate = (checks: TableCheck[]) => {
localTableChecks.value = checks;
};
const saveContentListener = () => { const saveContentListener = () => {
const hasModalOpen = !!document.querySelectorAll('.modal.active').length; const hasModalOpen = !!document.querySelectorAll('.modal.active').length;
if (props.isSelected && !hasModalOpen && isChanged.value) if (props.isSelected && !hasModalOpen && isChanged.value)

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