Compare commits

...

170 Commits

Author SHA1 Message Date
Fabio Di Stasio 49f1a8ef2e chore(release): 0.7.25 2024-06-19 08:57:50 +02:00
Fabio Di Stasio 121aa21a6d Merge branch 'beta' of https://github.com/antares-sql/antares 2024-06-19 08:57:07 +02:00
Fabio Di Stasio 3a47607a5f Merge branch 'master' of https://github.com/antares-sql/antares 2024-06-17 09:13:55 +02:00
Fabio Di Stasio 7494ff6fcf ci: changes on create-artifact-macos.yml 2024-06-17 09:13:31 +02:00
Fabio Di Stasio 838491bfd4 chore(release): 0.7.25-beta.2 2024-06-16 13:40:02 +02:00
Fabio Di Stasio 0b9898f3e7 feat(PostgreSQL): support to materialized views, closes #804 2024-06-14 18:05:29 +02:00
Fabio Di Stasio a973ec3c60 perf(UI): views grouped in folders 2024-06-13 18:07:05 +02:00
Fabio Di Stasio d0c50f17ca ci: temporary disabled auto test e2e 2024-06-10 08:52:00 +02:00
Fabio Di Stasio b4cdd58973 chore(release): 0.7.25-beta.1 2024-06-08 17:29:47 +02:00
Fabio Di Stasio 9947479fdc Merge branch 'beta' of https://github.com/antares-sql/antares into develop 2024-06-07 16:12:10 +02:00
Fabio Di Stasio 4a1697d633 fix: issue switching table after using a filter, fixes#691 2024-06-05 18:34:43 +02:00
Fabio Di Stasio b7dfd5cb8c build(deps): update various dependencies 2024-05-26 17:28:42 +02:00
Fabio Di Stasio 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
Fabio Di Stasio 3fa9873d20
Merge pull request #805 from mangas/upgrade-electron
chore: upgrade electron
2024-05-26 17:18:21 +02:00
Fabio Di Stasio 37a160a03f build(deps): update @electron/remote 2024-05-26 17:15:11 +02:00
Fabio Di Stasio 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
Fabio Di Stasio d1bb50b2bb fix(PostgreSQL): unable to search for databases, fixes #798 2024-05-08 08:56:23 +02:00
Fabio Di Stasio 8501fa2e81 Merge branch 'develop' of https://github.com/antares-sql/antares into develop 2024-05-07 18:16:30 +02:00
Fabio Di Stasio 25123e34ef fix: missing resizebars on mouse over 2024-05-07 18:15:32 +02:00
Fabio Di Stasio 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
Fabio Di Stasio 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
Fabio Di Stasio 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
Fabio Di Stasio 957cb9e1a5 chore(release): 0.7.24 2024-05-03 14:21:14 +02:00
Fabio Di Stasio 09c274a724 fix: missing accent color change 2024-05-02 18:00:07 +02:00
Fabio Di Stasio 9bcd874e80 chore(release): 0.7.24-beta.1 2024-04-30 18:09:52 +02:00
Fabio Di Stasio ece2ee05cc perf(UI): improvements on light theme 2024-04-30 18:08:07 +02:00
Fabio Di Stasio 058fc2fc0b feat: accent color based on folder color, closes #762 2024-04-30 18:07:08 +02:00
Fabio Di Stasio 33bbc0e7e6 fix(PostgreSQL): better handle connection errors, should fix #794 2024-04-30 18:06:11 +02:00
Fabio Di Stasio 23c59b4d4e fix(PostgreSQL): issue with similar tabs on differend databases 2024-04-18 18:22:29 +02:00
Fabio Di Stasio 6600197b82 perf(UI): hide "insert row" button in read-only mode, closes #695 2024-04-14 16:23:56 +02:00
Fabio Di Stasio 33203aeb04 refactor(UI): change query tab buttons order 2024-04-12 18:03:17 +02:00
Fabio Di Stasio f4f385589f chore(release): 0.7.24-beta.0 2024-04-12 08:44:08 +02:00
Fabio Di Stasio 0565ae1204 fix(translation): missing translation for "Open notes" shortcut 2024-04-08 18:33:37 +02:00
Fabio Di Stasio 258fbc81f7 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2024-04-08 18:30:23 +02:00
Fabio Di Stasio 8d8650fbe7 feat: unsaved file reminder closing file tabs 2024-04-08 18:29:05 +02:00
Fabio Di Stasio 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
Fabio Di Stasio 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
Fabio Di Stasio e7efb9c616 refactor(UI): change to query tab icons to avoid ambiguity with new features 2024-04-08 09:52:46 +02:00
Fabio Di Stasio 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
Fabio Di Stasio 1875e895ae chore(release): 0.7.23-beta.1 2024-04-02 09:10:17 +02:00
Fabio Di Stasio 2064294119 feat: add the page reference in the export file name, closes #772 2024-03-25 09:08:30 +01:00
Fabio Di Stasio 62e3115860 feat: move connections out of folder from context menu, related to #773 2024-03-24 11:10:00 +01:00
Fabio Di Stasio 9aef287a98 feat: move connections to folders from context menu, related to #773 2024-03-23 18:45:38 +01:00
Fabio Di Stasio 65ec4c5da6 fix: bad format of timestamp fields on CSV export, fixes 776 2024-03-23 16:33:19 +01:00
Fabio Di Stasio e19118982b chore(release): 0.7.23-beta.0 2024-03-21 23:09:09 +01:00
Fabio Di Stasio 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
Fabio Di Stasio de9dac3e8a fix: query result sort not working with aliased tables, fixes #765 2024-03-10 16:04:24 +01:00
Fabio Di Stasio dd5b41716a fix: CSV export does not escape strings when needed, fixes #770 2024-03-09 15:42:36 +01:00
Fabio Di Stasio 86acb390ac build: add husky and commitlint 2024-03-09 15:05:55 +01:00
Fabio Di Stasio 2884ec3dd6 chore(release): 0.7.22 2024-02-26 18:20:31 +01:00
Fabio Di Stasio b542a09c01 Merge branch 'beta' of https://github.com/antares-sql/antares 2024-02-26 18:19:35 +01:00
Fabio Di Stasio 6d94a04b67 Merge branch 'develop' of https://github.com/antares-sql/antares into beta 2024-02-26 18:19:14 +01:00
Fabio Di Stasio 8500fc40a1 refactor: improved note tab selection 2024-02-26 18:17:15 +01:00
Fabio Di Stasio 586f901bae fix: delete record modal pressing del when editing a field, fixes #767 2024-02-23 18:08:02 +01:00
Fabio Di Stasio 04e4d21e20 chore(release): 0.7.22-beta.2 2024-02-18 14:49:58 +01:00
Fabio Di Stasio fd3dd03eb2 chore: moved electron in devDependencies 2024-02-18 14:47:56 +01:00
Fabio Di Stasio d3f71e65ce feat(MySQL): option to enable single connection mode 2024-02-18 14:37:45 +01:00
Fabio Di Stasio 90b9b87b1d chore(deps): update various dependencies 2024-02-18 13:44:22 +01:00
Fabio Di Stasio e9b42c3edb chore(release): 0.7.22-beta.1 2024-02-12 18:31:01 +01:00
Fabio Di Stasio 259d051a21 fix: some issues related to previous commit 2024-02-12 18:30:07 +01:00
Fabio Di Stasio 876d5ea481 perf(MySQL): improvements in connection handling 2024-02-11 16:38:06 +01:00
Fabio Di Stasio 6d002efaf5
Update FUNDING.yml 2024-02-09 12:07:16 +01:00
Fabio Di Stasio 58be1abf5f
Update README.md 2024-02-09 09:27:16 +01:00
Fabio Di Stasio da56905572 refactor: improved SET field edit 2024-02-07 17:37:19 +01:00
Fabio Di Stasio d698f2798a fix: unable to edit tables containing SET fields, fixes #755 2024-02-06 18:16:26 +01:00
Fabio Di Stasio 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
Fabio Di Stasio 14eeaccb07 chore(release): 0.7.22-beta.0 2024-02-04 14:40:24 +01:00
Fabio Di Stasio 1a0c5da2f1 feat(UI): resizable textarea in new/edito note, closes #747 2024-02-04 14:38:15 +01:00
Fabio Di Stasio bb36e98beb perf(UI): improved notes, fixes #746 2024-01-20 10:11:49 +01:00
Fabio Di Stasio 8928510fb5 refactor: use Record to type objects 2024-01-19 18:03:20 +01:00
Fabio Di Stasio eb5d3f14f1 chore(release): 0.7.21 2024-01-13 16:31:14 +01:00
Fabio Di Stasio 33c127b090 Merge branch 'master' of https://github.com/antares-sql/antares 2024-01-13 16:30:23 +01:00
Fabio Di Stasio 4e98dc21d8 Merge branch 'beta' of https://github.com/antares-sql/antares 2024-01-13 16:30:21 +01:00
Fabio Di Stasio 20b27343cd feat(SQLite): enable schema reloat button on sidebar 2024-01-13 16:28:55 +01:00
Fabio Di Stasio 3b9228a723 fix(SQLite): unable to change integer fields length to 0, fixes #732 2024-01-13 16:28:31 +01:00
Fabio Di Stasio ab0f91b448 chore: remove Twitter links 2024-01-11 14:09:34 +01:00
Fabio286 0b6307c738 chore(release): 0.7.21-beta.1 2024-01-06 19:04:36 +01:00
Fabio286 dbf38fd99c ci: update create-generated-sources.yml 2024-01-06 18:53:47 +01:00
Fabio286 169fcb13da build(deps): downgrade better-sqlite3 2024-01-06 18:17:58 +01:00
Fabio Di Stasio 97ece32988 ci: action to generate generated-sources.json 2024-01-05 11:14:58 +01:00
Fabio Di Stasio c946c3fcda ci: update node version 2024-01-05 11:14:05 +01:00
Fabio Di Stasio cdd2a11f8e fix(PostgreSQL): unhandled error on connection lost, fixes #740 2023-12-29 14:42:12 +01:00
Fabio Di Stasio 23946ff2ce fix(PostgreSQL): exception deleting a table with one or less tabs open 2023-12-28 10:44:11 +01:00
Fabio Di Stasio 0f8d2cb4ef fix(PostgreSQL): error adding MONEY fields to a table 2023-12-28 10:13:28 +01:00
Fabio Di Stasio 219f89aa60 chore(release): 0.7.21-beta.0 2023-12-25 11:46:27 +01:00
Fabio Di Stasio eec29e99cc Merge branch 'master' of https://github.com/antares-sql/antares into beta 2023-12-25 11:46:13 +01:00
Fabio Di Stasio 171caed8b5 chore: minor docs changes 2023-12-25 11:40:52 +01:00
Fabio Di Stasio 88ec71c943
Merge pull request #735 from antares-sql/feat/new-scratchpad
Feat/new scratchpad
2023-12-25 11:19:42 +01:00
Fabio Di Stasio 532002ca01 refactor: migrate old scratchpad into notes 2023-12-25 11:19:23 +01:00
Fabio Di Stasio 9a732ea197 feat: open saved queries in a tab 2023-12-25 10:54:41 +01:00
Fabio Di Stasio b734b24679 fix: JavaScript error at first startup, fixes #736 2023-12-25 09:35:43 +01:00
Fabio Di Stasio a52fc3fd92 feat: buttons to save and access to saved queryes from query tab 2023-12-22 18:48:16 +01:00
Fabio Di Stasio bfa3924d57 feat: highlithg sql in notes, history and console 2023-12-22 18:06:27 +01:00
Fabio Di Stasio 08e5a13f72 feat: ability to edit notes 2023-12-21 18:10:51 +01:00
Fabio Di Stasio eaaf1b756a feat: new notes system 2023-12-21 10:16:46 +01:00
Fabio Di Stasio 84d221aaa7 chore: utility commit 2023-12-13 18:29:45 +01:00
Fabio Di Stasio ba6063e636 chore(release): 0.7.20 2023-12-08 13:08:29 +01:00
Fabio Di Stasio b055350726 fix: missing update indicator on setting icon 2023-12-08 13:02:15 +01:00
Fabio Di Stasio dbd533b229 Merge branch 'develop' of https://github.com/antares-sql/antares into feat/new-scratchpad 2023-12-06 08:53:35 +01:00
Fabio Di Stasio b5b35be45c chore(release): 0.7.20-beta.2 2023-12-06 08:52:48 +01:00
Fabio Di Stasio 6a72f6b4ae fix: communication with worker thread not working 2023-12-06 08:51:48 +01:00
Fabio Di Stasio 756786d72e chore: utility commit 2023-12-06 08:44:07 +01:00
Fabio Di Stasio 861b704344 chore(release): 0.7.20-beta.1 2023-12-02 14:22:32 +01:00
Fabio Di Stasio 9ce53165e8 chore: post merge cleanup 2023-12-02 14:21:34 +01:00
Fabio Di Stasio 62614dceb9
Merge pull request #727 from antares-sql/flatpak-experiments
Flatpak experiments
2023-12-02 14:12:51 +01:00
Fabio Di Stasio 8774dd44e6
Merge branch 'develop' into flatpak-experiments 2023-12-02 14:12:18 +01:00
Fabio Di Stasio f0ae01ca5e Merge branch 'develop' of https://github.com/antares-sql/antares into develop 2023-12-02 12:11:35 +01:00
Fabio286 03be777c2a refactor: worker threads to import sql dump instead of process 2023-12-02 11:35:20 +01:00
Fabio286 45a695ac0a refactor: improvements in worker implementation 2023-12-02 11:21:48 +01:00
Fabio286 c176841b75 refactor: worker threads to export sql dump instead of process 2023-12-02 09:31:54 +01:00
Fabio286 329246e2d8 refactor: minor refactor 2023-12-01 20:05:30 +01:00
Fabio Di Stasio e26809f260 fix(Flatpak): import/export schema not working 2023-12-01 14:18:40 +01:00
Fabio Di Stasio f13d4e6dce feat: copy element names on sidebar from context menu, closes #718 2023-11-29 18:15:22 +01:00
Fabio286 879de91516 refactor: minor refactor 2023-11-27 18:35:56 +01:00
Fabio Di Stasio 38b32bfb28 build: process.exit on devtoolsInstaller 2023-11-27 13:37:24 +01:00
Fabio Di Stasio 315d9d84c2 feat: logging errors on log file 2023-11-27 13:36:56 +01:00
Fabio286 c3d96cb35b refactor(Flatpak): temporarily disable import/export feature 2023-11-26 17:49:12 +01:00
Fabio286 05bd7672e1 chore: update package-lock.json 2023-11-26 17:28:45 +01:00
Fabio286 390bf88bb8 refactor: inport/export change for flatpak 2023-11-26 16:42:30 +01:00
Fabio Di Stasio 984aa893d3 refactor: temporary disable windows process validation 2023-11-24 10:18:44 +01:00
Fabio Di Stasio 1ac816eaa9 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2023-11-16 18:30:05 +01:00
Fabio Di Stasio 6f25fcbc05 ci: update workflow files 2023-11-16 18:16:41 +01:00
Fabio Di Stasio bc44465132 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2023-11-16 18:09:51 +01:00
Fabio Di Stasio 634a442213 ci: update workflow files 2023-11-16 18:01:40 +01:00
Fabio Di Stasio 93fe28d07d chore(release): 0.7.20-beta.0 2023-11-15 21:36:27 +01:00
Fabio Di Stasio 77a78078b2 chore(deps): update electron-builder 2023-11-14 15:14:59 +01:00
Fabio Di Stasio 0da31254a9 Merge branch 'master' of https://github.com/antares-sql/antares into beta 2023-11-14 14:36:50 +01:00
Fabio Di Stasio b1aeabf2b6 ci: update gh actions scripts 2023-11-14 14:36:31 +01:00
Fabio Di Stasio f1c857fca9 chore(deps): update better-sqlite3 2023-11-14 10:38:26 +01:00
Fabio Di Stasio 169f610b2e fix: error with multiple sessions in non-dev environment 2023-11-13 18:08:29 +01:00
Fabio Di Stasio 075f542dc8 feat: ability to open multiple app sessions 2023-11-12 18:05:11 +01:00
Fabio Di Stasio 664b2181be
Merge pull request #712 from 64knl/feat/update-nl-translation
feat: Update Dutch translation
2023-11-12 15:43:15 +01:00
Fabio Di Stasio 1bc95b0c2c fix: missing open folder icon for trigger, function and other database elements on sidebar 2023-11-10 18:12:07 +01:00
Rene 8628711374
feat: nl string updates 2023-11-10 15:21:22 +01:00
Fabio Di Stasio 186fc18363 fix(Firebird SQL): error "Cannot read properties of null" connecting to some databases, fixes #708 2023-11-09 18:49:56 +01:00
Fabio Di Stasio e14302bdc0 feat(translation): add Ukrainian language, thanks to #707 2023-11-06 18:01:19 +01:00
Fabio Di Stasio 13afc8bffd
Merge pull request #710 from antares-sql/all-contributors/add-zvlad
docs: add zvlad as a contributor for translation
2023-11-06 09:15:12 +01:00
allcontributors[bot] 811be75c73
docs: update .all-contributorsrc [skip ci] 2023-11-06 08:14:38 +00:00
allcontributors[bot] bddec52b40
docs: update README.md [skip ci] 2023-11-06 08:14:37 +00:00
Fabio Di Stasio e34af5bfa4 chore: update README.md 2023-11-03 18:26:58 +01:00
Fabio Di Stasio 3322a06fed
Merge pull request #703 from jimcat8/cn_main
Updated zh-CN.ts file
2023-11-03 13:17:19 +01:00
tianci 74040cee44
update 2023-11-02 17:52:13 +08:00
tianci 27c904e7f4
Updated zh-CN.ts file 2023-11-02 16:51:13 +08:00
Fabio Di Stasio 6876dd6063 chore: add flathub badge in README.md 2023-11-02 09:25:13 +01:00
Fabio Di Stasio 4d676a41cd chode: add flatpak assets 2023-11-01 12:12:14 +01:00
Fabio Di Stasio 19d9f56f08 chore(release): 0.7.19 2023-11-01 09:43:50 +01:00
Fabio Di Stasio 4c89578188 Merge branch 'develop' of https://github.com/antares-sql/antares into beta 2023-11-01 09:42:59 +01:00
Fabio Di Stasio f6fb266771 fix: table field changes not saved on text fields if pressing enter on textarea modal 2023-10-31 17:57:58 +01:00
Fabio Di Stasio 3e739bcaa2 chore(release): 0.7.19-beta.2 2023-10-29 11:47:08 +01:00
Fabio Di Stasio debc1da289 fix: ssh tunnel keep-alive not working properly 2023-10-28 19:09:19 +02:00
Fabio Di Stasio 3c2e2be40f chore: regenerate package-lock.json 2023-10-28 18:47:06 +02:00
Fabio Di Stasio 581ec6a25d Merge branch 'develop' of https://github.com/antares-sql/antares into develop 2023-10-28 18:25:03 +02:00
Fabio Di Stasio d30a978cd6 refactor: replace ssh2-promise with @fabio286/ssh2-promise 2023-10-28 18:25:01 +02:00
Fabio286 0015f2e860 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2023-10-28 11:58:06 +02:00
Fabio286 389e6624d8 chore: update stylelint dependencies 2023-10-28 11:55:18 +02:00
147 changed files with 9256 additions and 3523 deletions

View File

@ -257,6 +257,42 @@
"contributions": [ "contributions": [
"translation" "translation"
] ]
},
{
"login": "zvlad",
"name": "Vladyslav",
"avatar_url": "https://avatars.githubusercontent.com/u/9055134?v=4",
"profile": "https://github.com/zvlad",
"contributions": [
"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"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

4
.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
@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
liberapay: # Replace with a single Liberapay username liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username otechie: # Replace with a single Otechie username
custom: ['https://paypal.me/fabiodistasio'] custom: ['https://paypal.me/fabiodistasio']

View File

@ -13,8 +13,9 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false
matrix: matrix:
os: [macos-11, ubuntu-latest, windows-latest] os: [macos-latest, ubuntu-latest, windows-latest]
steps: steps:
- name: Check out Git repository - name: Check out Git repository
@ -25,7 +26,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: Install dependencies - name: Install dependencies
run: npm i run: npm i

View File

@ -13,8 +13,9 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false
matrix: matrix:
os: [macos-11, ubuntu-latest, windows-latest] os: [macos-latest, ubuntu-latest, windows-latest]
steps: steps:
- name: Exit if not on master branch - name: Exit if not on master branch
@ -31,7 +32,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

View File

@ -15,7 +15,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: Install dependencies - name: Install dependencies
run: npm i run: npm i

View File

@ -5,7 +5,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -13,7 +13,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

View File

@ -13,11 +13,12 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 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
@ -29,3 +30,31 @@ jobs:
build build
!build/*-unpacked !build/*-unpacked
!build/.icon-ico !build/.icon-ico
build-beta:
runs-on: macos-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
with:
ref: beta
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- name: npm install & build
run: |
npm install
npm install "dmg-license" --save-optional
npm run build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: macos-build-develop
retention-days: 3
path: |
build
!build/*-unpacked
!build/.icon-ico

View File

@ -0,0 +1,50 @@
name: Create generated-rources.json
on:
workflow_dispatch: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
# Install flatpak-node-generator
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: '3.8'
- name: Install pipx
uses: CfirTsabari/actions-pipx@v1
- name: Install flatpak-node-generator
run: |
cd ../
git clone https://github.com/flatpak/flatpak-builder-tools.git
cd flatpak-builder-tools/node
pipx install .
# Install Antares
- name: Check out Git repository
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
# - name: Delete old package-lock.json
# run: rm package-lock.json
- name: Install dependencies
run: npm i --lockfile-version 2
- name: Generate generated-sources.json
run: flatpak-node-generator npm -r package-lock.json --electron-node-headers
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: generated-sources
retention-days: 3
path: |
generated-sources.json

33
.github/workflows/test-builds.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: Test build [DEVELOP]
on:
workflow_dispatch: {}
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v3
with:
ref: develop
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- name: Install dependencies
run: npm i
- name: "Build"
run: npm run build

View File

@ -1,9 +1,10 @@
name: Test end-to-end [WINDOWS] name: Test end-to-end
on: on:
push: workflow_dispatch: {}
branches: # push:
- master # branches:
# - develop
jobs: jobs:
release: release:
@ -20,7 +21,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

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

BIN
.nvmrc Normal file

Binary file not shown.

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

9
.vscode/launch.json vendored
View File

@ -17,15 +17,6 @@
"sourceMaps": true, "sourceMaps": true,
"type": "chrome", "type": "chrome",
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}"
},
{
"name": "Electron: Worker",
"cwd": "${workspaceFolder}",
"port": 9224,
"request": "attach",
"sourceMaps": true,
"type": "node",
"timeout": 1000000
} }
], ],
"compounds": [ "compounds": [

View File

@ -10,7 +10,8 @@
"translation", "translation",
"Linux", "Linux",
"MacOS", "MacOS",
"deps" "deps",
"Flatpak"
], ],
"svg.preview.background": "transparent" "svg.preview.background": "transparent"
} }

View File

@ -2,6 +2,244 @@
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.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)
### Features
* **SQLite:** enable schema reloat button on sidebar ([20b2734](https://github.com/antares-sql/antares/commit/20b27343cd95998bd83403b7556ea35fcad9fa1b))
### Bug Fixes
* **SQLite:** unable to change integer fields length to 0, fixes [#732](https://github.com/antares-sql/antares/issues/732) ([3b9228a](https://github.com/antares-sql/antares/commit/3b9228a7230dcd9f47f5794a83b60d28207bdce1))
### [0.7.21-beta.1](https://github.com/antares-sql/antares/compare/v0.7.21-beta.0...v0.7.21-beta.1) (2024-01-06)
### Bug Fixes
* **PostgreSQL:** error adding MONEY fields to a table ([0f8d2cb](https://github.com/antares-sql/antares/commit/0f8d2cb4ef5c327f96f788179be0b309689b4ce8))
* **PostgreSQL:** exception deleting a table with one or less tabs open ([23946ff](https://github.com/antares-sql/antares/commit/23946ff2cef6d63e1529e2c8c4357d7fdedc3284))
* **PostgreSQL:** unhandled error on connection lost, fixes [#740](https://github.com/antares-sql/antares/issues/740) ([cdd2a11](https://github.com/antares-sql/antares/commit/cdd2a11f8e33d6607337989723774d60c7c1a030))
### [0.7.21-beta.0](https://github.com/antares-sql/antares/compare/v0.7.20...v0.7.21-beta.0) (2023-12-25)
### Features
* ability to edit notes ([08e5a13](https://github.com/antares-sql/antares/commit/08e5a13f723bc3ae95b0f529b79f0b558bc2a377))
* buttons to save and access to saved queryes from query tab ([a52fc3f](https://github.com/antares-sql/antares/commit/a52fc3fd923fec30cfdd3d804554e6fe4534c400))
* highlithg sql in notes, history and console ([bfa3924](https://github.com/antares-sql/antares/commit/bfa3924d57c2ea2cc2857006d6bd6279865dbc99))
* new notes system ([eaaf1b7](https://github.com/antares-sql/antares/commit/eaaf1b756a6b5ffb77f7f07f3e4c0971822d48c3))
* open saved queries in a tab ([9a732ea](https://github.com/antares-sql/antares/commit/9a732ea1971d223f3278ad02d3dd77837fecb377))
### Bug Fixes
* JavaScript error at first startup, fixes [#736](https://github.com/antares-sql/antares/issues/736) ([b734b24](https://github.com/antares-sql/antares/commit/b734b246795fb240f6728714be68c22cc221bbe9))
### [0.7.20](https://github.com/antares-sql/antares/compare/v0.7.20-beta.2...v0.7.20) (2023-12-08)
### Bug Fixes
* missing update indicator on setting icon ([b055350](https://github.com/antares-sql/antares/commit/b055350726774e05a4e04ea6d890c46f64f2112e))
### [0.7.20-beta.2](https://github.com/antares-sql/antares/compare/v0.7.20-beta.1...v0.7.20-beta.2) (2023-12-06)
### Bug Fixes
* communication with worker thread not working ([6a72f6b](https://github.com/antares-sql/antares/commit/6a72f6b4ae3f4c1d6c42ca7a817d2f2c135696a7))
### [0.7.20-beta.1](https://github.com/antares-sql/antares/compare/v0.7.20-beta.0...v0.7.20-beta.1) (2023-12-02)
### Features
* copy element names on sidebar from context menu, closes [#718](https://github.com/antares-sql/antares/issues/718) ([f13d4e6](https://github.com/antares-sql/antares/commit/f13d4e6dceb70b0c7cb8d56ddfb5f00e938571cc))
* logging errors on log file ([315d9d8](https://github.com/antares-sql/antares/commit/315d9d84c2caa29852d68bd189750b2a4028d953))
### Bug Fixes
* **Flatpak:** import/export schema not working ([e26809f](https://github.com/antares-sql/antares/commit/e26809f260099ba194bf5d00671cae14d438197b))
### [0.7.20-beta.0](https://github.com/antares-sql/antares/compare/v0.7.19...v0.7.20-beta.0) (2023-11-15)
### Features
* ability to open multiple app sessions ([075f542](https://github.com/antares-sql/antares/commit/075f542dc8f4a48bef07b86b78c40d03fcdccc56))
* nl string updates ([8628711](https://github.com/antares-sql/antares/commit/8628711374269c29c4b1e6722fe66b0d8179477e))
* **translation:** add Ukrainian language, thanks to [#707](https://github.com/antares-sql/antares/issues/707) ([e14302b](https://github.com/antares-sql/antares/commit/e14302bdc0038b84a1a06089753205149cd1a92b))
### Bug Fixes
* error with multiple sessions in non-dev environment ([169f610](https://github.com/antares-sql/antares/commit/169f610b2ee4857661ec3da7f04b628fec21f1f0))
* **Firebird SQL:** error "Cannot read properties of null" connecting to some databases, fixes [#708](https://github.com/antares-sql/antares/issues/708) ([186fc18](https://github.com/antares-sql/antares/commit/186fc18363b6f14678465a8e38d85b1319e47b50))
* missing open folder icon for trigger, function and other database elements on sidebar ([1bc95b0](https://github.com/antares-sql/antares/commit/1bc95b0c2cd91bbf0410a23266e23bbbf2a71995))
### [0.7.19](https://github.com/antares-sql/antares/compare/v0.7.19-beta.2...v0.7.19) (2023-11-01)
### Bug Fixes
* table field changes not saved on text fields if pressing enter on textarea modal ([f6fb266](https://github.com/antares-sql/antares/commit/f6fb266771f2d798c8ae42b997c1e33520cf21c3))
### [0.7.19-beta.2](https://github.com/antares-sql/antares/compare/v0.7.19-beta.1...v0.7.19-beta.2) (2023-10-29)
### Bug Fixes
* ssh tunnel keep-alive not working properly ([debc1da](https://github.com/antares-sql/antares/commit/debc1da289d5e35d59adf69d094b329cf93af536))
### [0.7.19-beta.1](https://github.com/antares-sql/antares/compare/v0.7.19-beta.0...v0.7.19-beta.1) (2023-10-25) ### [0.7.19-beta.1](https://github.com/antares-sql/antares/compare/v0.7.19-beta.0...v0.7.19-beta.1) (2023-10-25)

View File

@ -7,8 +7,7 @@
# 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) [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Ffabio286%2Fantares%2Fbadge&style=flat)](https://actions-badge.atrox.dev/fabio286/antares/goto) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares) ![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)
![Mastodon Follow](https://img.shields.io/mastodon/follow/%20110860460902482117?domain=https%3A%2F%2Ffosstodon.org&style=social) [![Twitter Follow](https://img.shields.io/twitter/follow/AntaresSQL?style=social)](https://twitter.com/AntaresSQL) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. 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.
@ -18,10 +17,11 @@ However, there are all the features necessary to have a pleasant database manage
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible. We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible.
🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest). 🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest).
👁 To stay tuned for new releases follow Antares SQL on [Mastodon](https://fosstodon.org/@AntaresSQL) or [Twitter](https://twitter.com/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.
🗳️ Polls: 🗳️ Polls:
- **[Which is the main OS you use Antares on?](https://github.com/antares-sql/antares/discussions/379)** - **[Which is the main OS you use Antares on?](https://github.com/antares-sql/antares/discussions/379)**
- **[Which database do you use the most?](https://github.com/antares-sql/antares/discussions/594)** - **[Which database do you use the most?](https://github.com/antares-sql/antares/discussions/594)**
@ -35,6 +35,7 @@ We are actively working on it, hoping to provide new cool features, improvements
- Fake table data filler to generate tons of data for test purpose. - Fake table data filler to generate tons of data for test purpose.
- Query suggestions and auto complete. - Query suggestions and auto complete.
- Query history: search through the last 1000 queries. - Query history: search through the last 1000 queries.
- Save queries, notes or todo.
- SSH tunnel support. - SSH tunnel support.
- Manual commit mode. - Manual commit mode.
- Import and export database dumps. - Import and export database dumps.
@ -55,7 +56,7 @@ Since Antares SQL is a free software we don't have a budget to spend on annual l
### Linux ### Linux
On Linux you can simply download and run the `.AppImage` distribution, install from Snap Store, from AUR or from our [PPA repository](https://github.com/antares-sql/antares-ppa). On Linux you can simply download and run the `.AppImage` distribution, install from FlatHub, Snap Store, AUR or from our [PPA repository](https://github.com/antares-sql/antares-ppa).
### Windows ### Windows
@ -67,19 +68,8 @@ On macOS you can run `.dmg` distribution following [this guide](https://support.
## Download ## Download
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from AUR](https://raw.githubusercontent.com/Fabio286/antares/3e00c4bae6e036300c752c1a40c5a038fea9c169/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql-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/Fabio286/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
@ -89,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...
@ -110,7 +101,7 @@ This is a roadmap with major features will come in near future.
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares) - 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide) - 📖 [Contributors Guide](https://github.com/Fabio286/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 ✨
@ -155,6 +146,12 @@ 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/zxp19821005"><img src="https://avatars.githubusercontent.com/u/4915850?v=4?s=100" width="100px;" alt="Woodenman"/><br /><sub><b>Woodenman</b></sub></a><br /><a href="#platform-zxp19821005" title="Packaging/porting to new platform">📦</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/zxp19821005"><img src="https://avatars.githubusercontent.com/u/4915850?v=4?s=100" width="100px;" alt="Woodenman"/><br /><sub><b>Woodenman</b></sub></a><br /><a href="#platform-zxp19821005" title="Packaging/porting to new platform">📦</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markusand"><img src="https://avatars.githubusercontent.com/u/12972543?v=4?s=100" width="100px;" alt="Marc Vilella"/><br /><sub><b>Marc Vilella</b></sub></a><br /><a href="#translation-markusand" title="Translation">🌍</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/markusand"><img src="https://avatars.githubusercontent.com/u/12972543?v=4?s=100" width="100px;" alt="Marc Vilella"/><br /><sub><b>Marc Vilella</b></sub></a><br /><a href="#translation-markusand" 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/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>
</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>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -0,0 +1,9 @@
[Desktop Entry]
Name=Antares SQL
Exec=startantares
Terminal=false
Type=Application
Icon=it.fabiodistasio.AntaresSQL
StartupWMClass=antares
Comment=A modern, fast and productivity driven SQL client with a focus in UX
Categories=Development;

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>it.fabiodistasio.AntaresSQL</id>
<name>Antares SQL</name>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
<developer_name>Fabio Di Stasio</developer_name>
<summary>A modern, fast and productivity driven SQL client with a focus in UX</summary>
<url type="homepage">https://antares-sql.app/</url>
<url type="bugtracker">https://github.com/antares-sql/antares/issues</url>
<url type="help">https://github.com/antares-sql/antares/discussions</url>
<url type="donation">https://paypal.me/fabiodistasio</url>
<description>
<p>Antares is an SQL client that aims to become an useful and complete tool, especially for developers. </p>
<p>The main goal is to develop a totally free, full featured, cross platform and open source alternative.
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenu; productivity comes first.</p>
<p>Supported database: </p>
<ul>
<li>MySQL/MariaDB</li>
<li>PostgreSQL</li>
<li>SQLite</li>
<li>Firebird SQL</li>
</ul>
</description>
<screenshots>
<screenshot type="default">
<image type="source">https://lh3.googleusercontent.com/drive-viewer/AK7aPaC00fbmJIUcfwSPv-hjoxEmHS8NapR8qyOqOpopMIdcDFqYKNDs5mdIK08hnhZdHMrozTfR4Hx3Yj6bQ0zgfStEEFhxWg=s1600</image>
</screenshot>
</screenshots>
<content_rating type="oars-1.1" />
<releases>
<release version="v0.7.19" date="2023-11-01"/>
</releases>
</component>

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

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'
]
]
}
};

View File

@ -1,76 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" style="isolation:isolate" width="182" height="56">
<!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
style="isolation:isolate" viewBox="0 0 182 56" width="182px" height="56px">
<defs> <defs>
<clipPath id="_clipPath_tR1uglJ1Zei76xP861DY1TsjAiQWS9qF"> <clipPath id="prefix__a">
<rect width="182" height="56" /> <path d="M0 0h182v56H0z" />
</clipPath> </clipPath>
</defs> </defs>
<g clip-path="url(#_clipPath_tR1uglJ1Zei76xP861DY1TsjAiQWS9qF)"> <g clip-path="url(#prefix__a)">
<path <!-- <path d="M2.5.5h178a2 2 0 012 2v52a2 2 0 01-2 2H2.5a2 2 0" fill="#252525" stroke="#FFF" stroke-width="2"/> -->
d="M 2.5 0.5 L 180.5 0.5 C 181.604 0.5 182.5 1.396 182.5 2.5 L 182.5 54.5 C 182.5 55.604 181.604 56.5 180.5 56.5 L 2.5 56.5 C 1.396 56.5 0.5 55.604 0.5 54.5 L 0.5 2.5 C 0.5 1.396 1.396 0.5 2.5 0.5 Z" <rect x="0" y="0" width="182" height="56" fill="#252525" stroke="#FFF" stroke-width="1" rx="0" />
style="stroke:none;fill:#252525;stroke-miterlimit:10;" />
<path <g fill-rule="evenodd" fill="#FFF">
d="M 2.5 0.5 L 180.5 0.5 C 181.604 0.5 182.5 1.396 182.5 2.5 L 182.5 54.5 C 182.5 55.604 181.604 56.5 180.5 56.5 L 2.5 56.5 C 1.396 56.5 0.5 55.604 0.5 54.5 L 0.5 2.5 C 0.5 1.396 1.396 0.5 2.5 0.5 Z"
style="fill:none;stroke:#CDCDCD;stroke-width:1;stroke-miterlimit:2;" />
<g>
<g>
<path
d=" M 60.898 13.777 C 58.555 13.774 56.61 14.254 55.858 14.516 L 55.083 18.697 C 55.081 18.712 58.937 17.669 60.635 17.73 C 63.447 17.831 63.706 18.805 63.656 20.119 C 63.704 20.196 62.931 18.931 60.498 18.889 C 57.43 18.836 53.098 19.976 53.104 24.608 C 53.022 29.818 56.997 31.351 59.704 31.379 C 62.138 31.335 63.279 30.458 63.904 29.988 C 64.726 29.129 65.665 28.265 66.561 27.229 C 65.714 28.77 64.978 29.835 64.213 30.651 L 64.213 31.339 L 67.913 30.716 L 67.938 20.66 C 67.901 19.237 68.754 13.791 60.898 13.777 Z M 60.367 22.533 C 61.9 22.554 63.659 23.31 63.662 25.129 C 63.669 26.784 61.589 27.674 60.235 27.66 C 58.881 27.646 57.085 26.596 57.077 24.982 C 57.103 23.54 58.771 22.496 60.367 22.533 Z "
fill-rule="evenodd" fill="rgb(255,255,255)" />
<path
d=" M 70.378 14.707 L 70.352 31.36 L 74.662 30.529 L 74.67 21.087 C 74.671 19.681 76.679 18.039 79.198 18.065 C 79.733 17.097 80.738 14.625 80.983 14.062 C 75.354 14.049 75.282 15.68 74.303 16.483 C 74.293 14.952 74.3 14.033 74.3 14.033 L 70.378 14.707 L 70.378 14.707 Z "
fill-rule="evenodd" fill="rgb(255,255,255)" />
<path
d=" M 94.632 16.893 C 94.591 16.873 92.385 14.312 87.949 14.292 C 83.795 14.223 79.135 15.834 79.061 22.8 C 79.097 28.925 83.537 31.318 87.973 31.365 C 92.72 31.414 94.609 28.396 94.722 28.322 C 94.156 27.83 92.034 25.728 92.034 25.728 C 92.034 25.728 90.709 27.615 88.138 27.639 C 85.566 27.664 83.331 25.651 83.299 22.844 C 83.266 20.036 85.354 18.515 88.157 18.392 C 90.584 18.392 91.984 19.959 91.984 19.959 L 94.632 16.893 L 94.632 16.893 Z "
fill-rule="evenodd" fill="rgb(255,255,255)" />
<path
d=" M 100.065 8.879 L 95.996 9.835 L 96.026 31.526 L 100.034 30.802 L 100.08 20.595 C 100.089 19.524 101.628 17.88 104.2 17.933 C 106.658 17.958 107.207 19.571 107.201 19.775 L 107.272 31.592 L 111.224 30.894 L 111.239 18.363 C 111.265 17.157 108.598 14.61 104.311 14.592 C 102.273 14.596 101.145 15.057 100.571 15.397 C 99.588 16.156 98.466 16.883 97.362 17.811 C 98.382 16.501 99.239 15.595 100.075 14.921 L 100.065 8.879 L 100.065 8.879 Z "
fill-rule="evenodd" fill="rgb(255,255,255)" />
</g>
<g>
<path
d=" M 114.673 9.441 L 116.508 8.982 L 116.595 30.85 L 114.73 31.168 L 114.673 9.441 L 114.673 9.441 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
<path
d=" M 119.663 15.968 L 121.271 15.252 L 121.285 30.932 L 119.731 31.253 L 119.663 15.968 L 119.663 15.968 Z M 119.28 10.314 L 120.577 9.255 L 121.655 10.454 L 120.357 11.54 L 119.28 10.314 L 119.28 10.314 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
<path
d=" M 124.296 15.682 L 126.131 15.308 L 126.14 18.586 C 126.14 18.727 127.148 14.924 132.008 15.009 C 136.727 15.035 137.499 18.688 137.473 19.507 L 137.531 31.034 L 135.922 31.384 L 135.913 19.998 C 135.932 19.665 135.178 16.853 131.843 16.843 C 128.509 16.833 126.199 19.265 126.203 20.818 L 126.229 30.848 L 124.365 31.335 L 124.296 15.682 L 124.296 15.682 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
<path
d=" M 153.547 31.117 L 151.711 31.492 L 151.703 28.214 C 151.703 28.073 150.694 31.876 145.835 31.791 C 141.116 31.765 140.344 28.112 140.37 27.293 L 140.311 15.765 L 142.261 15.372 L 142.292 26.758 C 142.292 27.069 142.665 29.947 145.999 29.957 C 149.334 29.967 151.669 27.949 151.686 24.911 L 151.662 15.928 L 153.477 15.464 L 153.547 31.117 L 153.547 31.117 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
<path
d=" M 157.144 15.553 L 155.857 16.56 L 160.792 23.018 L 155.529 30.478 L 156.894 31.492 L 161.841 24.563 L 166.948 31.656 L 168.211 30.649 L 162.738 23.065 L 167.104 16.933 L 165.762 15.797 L 161.785 21.472 L 157.144 15.553 L 157.144 15.553 Z "
fill-rule="evenodd" fill="rgb(23,147,209)" />
</g>
<path <path
d=" M 33.112 3.879 C 30.965 9.143 29.67 12.587 27.279 17.695 C 28.745 19.248 30.544 21.058 33.466 23.101 C 30.325 21.809 28.182 20.511 26.581 19.164 C 23.521 25.549 18.728 34.643 9 52.121 C 16.645 47.707 22.572 44.986 28.095 43.948 C 27.858 42.928 27.723 41.824 27.733 40.673 L 27.742 40.428 C 27.863 35.53 30.411 31.763 33.43 32.019 C 36.448 32.274 38.794 36.455 38.673 41.353 C 38.65 42.275 38.546 43.162 38.364 43.984 C 43.828 45.053 49.691 47.767 57.233 52.121 C 55.746 49.383 54.419 46.915 53.151 44.565 C 51.154 43.017 49.072 41.003 44.823 38.822 C 47.743 39.581 49.834 40.456 51.464 41.435 C 38.575 17.439 37.532 14.251 33.112 3.879 Z " d="M60.898 13.777c-2.343-.003-4.288.477-5.04.739l-.775 4.181c-.002.015 3.854-1.028 5.552-.967 2.812.101 3.071 1.075 3.021 2.389.048.077-.725-1.188-3.158-1.23-3.068-.053-7.4 1.087-7.394 5.719-.082 5.21 3.893 6.743 6.6 6.771 2.434-.044 3.575-.921 4.2-1.391.822-.859 1.761-1.723 2.657-2.759-.847 1.541-1.583 2.606-2.348 3.422v.688l3.7-.623.025-10.056c-.037-1.423.816-6.869-7.04-6.883zm-.531 8.756c1.533.021 3.292.777 3.295 2.596.007 1.655-2.073 2.545-3.427 2.531-1.354-.014-3.15-1.064-3.158-2.678.026-1.442 1.694-2.486 3.29-2.449zM70.378 14.707l-.026 16.653 4.31-.831.008-9.442c.001-1.406 2.009-3.048 4.528-3.022.535-.968 1.54-3.44 1.785-4.003-5.629-.013-5.701 1.618-6.68 2.421-.01-1.531-.003-2.45-.003-2.45l-3.922.674zM94.632 16.893c-.041-.02-2.247-2.581-6.683-2.601-4.154-.069-8.814 1.542-8.888 8.508.036 6.125 4.476 8.518 8.912 8.565 4.747.049 6.636-2.969 6.749-3.043-.566-.492-2.688-2.594-2.688-2.594s-1.325 1.887-3.896 1.911c-2.572.025-4.807-1.988-4.839-4.795-.033-2.808 2.055-4.329 4.858-4.452 2.427 0 3.827 1.567 3.827 1.567l2.648-3.066zM100.065 8.879l-4.069.956.03 21.691 4.008-.724.046-10.207c.009-1.071 1.548-2.715 4.12-2.662 2.458.025 3.007 1.638 3.001 1.842l.071 11.817 3.952-.698.015-12.531c.026-1.206-2.641-3.753-6.928-3.771-2.038.004-3.166.465-3.74.805-.983.759-2.105 1.486-3.209 2.414 1.02-1.31 1.877-2.216 2.713-2.89l-.01-6.042z" />
fill-rule="evenodd" fill="rgb(23,147,209)" />
<g>
<path
d=" M 170.614 30.156 L 170.614 28.802 L 170.109 28.802 L 170.109 28.621 L 171.325 28.621 L 171.325 28.802 L 170.817 28.802 L 170.817 30.156 L 170.614 30.156 Z "
fill="rgb(23,147,209)" />
<path
d=" M 171.536 30.156 L 171.536 28.621 L 171.842 28.621 L 172.205 29.708 C 172.238 29.809 172.263 29.884 172.278 29.935 C 172.295 29.879 172.323 29.797 172.36 29.689 L 172.727 28.621 L 173 28.621 L 173 30.156 L 172.804 30.156 L 172.804 28.871 L 172.358 30.156 L 172.175 30.156 L 171.732 28.849 L 171.732 30.156 L 171.536 30.156 Z "
fill="rgb(23,147,209)" />
</g>
<g>
<path
d=" M 57.471 47.815 L 57.471 46.493 L 56.977 46.493 L 56.977 46.316 L 58.166 46.316 L 58.166 46.493 L 57.67 46.493 L 57.67 47.815 L 57.471 47.815 Z "
fill="rgb(23,147,209)" />
<path
d=" M 58.372 47.815 L 58.372 46.316 L 58.671 46.316 L 59.026 47.377 C 59.059 47.476 59.083 47.55 59.098 47.599 C 59.115 47.545 59.141 47.465 59.177 47.359 L 59.536 46.316 L 59.803 46.316 L 59.803 47.815 L 59.612 47.815 L 59.612 46.56 L 59.176 47.815 L 58.997 47.815 L 58.564 46.539 L 58.564 47.815 L 58.372 47.815"
fill="rgb(23,147,209)" />
</g>
</g> </g>
<g clip-path="url(#_clipPath_NvFIpNfWUS6M4fZAtfyVzggsKR3URDoi)"><text transform="matrix(1,0,0,1,87.023,43.899)" <g fill-rule="evenodd" fill="#1793D1">
style="font-family:'Open Sans';font-weight:700;font-size:11px;font-style:normal;fill:#ffffff;stroke:none;">user <path
repository</text></g> d="M114.673 9.441l1.835-.459.087 21.868-1.865.318-.057-21.727zM119.663 15.968l1.608-.716.014 15.68-1.554.321-.068-15.285zm-.383-5.654l1.297-1.059 1.078 1.199-1.298 1.086-1.077-1.226zM124.296 15.682l1.835-.374.009 3.278c0 .141 1.008-3.662 5.868-3.577 4.719.026 5.491 3.679 5.465 4.498l.058 11.527-1.609.35-.009-11.386c.019-.333-.735-3.145-4.07-3.155-3.334-.01-5.644 2.422-5.64 3.975l.026 10.03-1.864.487-.069-15.653zM153.547 31.117l-1.836.375-.008-3.278c0-.141-1.009 3.662-5.868 3.577-4.719-.026-5.491-3.679-5.465-4.498l-.059-11.528 1.95-.393.031 11.386c0 .311.373 3.189 3.707 3.199 3.335.01 5.67-2.008 5.687-5.046l-.024-8.983 1.815-.464.07 15.653zM157.144 15.553l-1.287 1.007 4.935 6.458-5.263 7.46 1.365 1.014 4.947-6.929 5.107 7.093 1.263-1.007-5.473-7.584 4.366-6.132-1.342-1.136-3.977 5.675-4.641-5.919z" />
</g>
<path
d="M33.112 3.879c-2.147 5.264-3.442 8.708-5.833 13.816 1.466 1.553 3.265 3.363 6.187 5.406-3.141-1.292-5.284-2.59-6.885-3.937C23.521 25.549 18.728 34.643 9 52.121c7.645-4.414 13.572-7.135 19.095-8.173a13.965 13.965 0 01-.362-3.275l.009-.245c.121-4.898 2.669-8.665 5.688-8.409 3.018.255 5.364 4.436 5.243 9.334a13.819 13.819 0 01-.309 2.631c5.464 1.069 11.327 3.783 18.869 8.137-1.487-2.738-2.814-5.206-4.082-7.556-1.997-1.548-4.079-3.562-8.328-5.743 2.92.759 5.011 1.634 6.641 2.613C38.575 17.439 37.532 14.251 33.112 3.879z"
fill-rule="evenodd" fill="#1793D1" />
<g fill="#1793D1">
<path
d="M170.614 30.156v-1.354h-.505v-.181h1.216v.181h-.508v1.354h-.203zM171.536 30.156v-1.535h.306l.363 1.087c.033.101.058.176.073.227a8.63 8.63 0 01.082-.246l.367-1.068H173v1.535h-.196v-1.285l-.446 1.285h-.183l-.443-1.307v1.307h-.196z" />
</g>
<g fill="#1793D1">
<path
d="M57.471 47.815v-1.322h-.494v-.177h1.189v.177h-.496v1.322h-.199zM58.372 47.815v-1.499h.299l.355 1.061.072.222c.017-.054.043-.134.079-.24l.359-1.043h.267v1.499h-.191V46.56l-.436 1.255h-.179l-.433-1.276v1.276h-.192" />
</g>
<g clip-path="url(#prefix__b)"><text transform="translate(95.023 43.899)" font-family="sans-serif"
font-weight="700" font-size="10" fill="#fff">user repository</text></g>
<defs> <defs>
<clipPath id="_clipPath_NvFIpNfWUS6M4fZAtfyVzggsKR3URDoi"> <clipPath id="prefix__b">
<rect x="0" y="0" width="86" height="14.98" transform="matrix(1,0,0,1,87,32.142)" /> <path transform="translate(87 32.142)" d="M0 0h86v14.98H0z" />
</clipPath> </clipPath>
</defs> </defs>
</g> </g>

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

6241
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.19-beta.1", "version": "0.7.25",
"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,44 +119,70 @@
} }
}, },
"dependencies": { "dependencies": {
"@electron/remote": "~2.0.1", "@electron/remote": "~2.1.2",
"@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.0", "babel-loader": "~8.2.3",
"electron-log": "~4.4.1", "better-sqlite3": "~10.0.0",
"chalk": "~4.1.2",
"cpu-features": "^0.0.10",
"cross-env": "~7.0.2",
"css-loader": "~6.5.0",
"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",
"node-loader": "~2.0.0",
"pg": "~8.11.5",
"pg-connection-string": "~2.5.0", "pg-connection-string": "~2.5.0",
"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",
"ssh2-promise": "~1.0.2", "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.91.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",
@ -165,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": "~22.10.3",
"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",
@ -180,38 +202,17 @@
"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": "~14.9.1", "stylelint-config-recommended-vue": "~1.5.0",
"stylelint-config-recommended-vue": "~1.4.0", "stylelint-config-standard": "~34.0.0",
"stylelint-config-standard": "~26.0.0", "stylelint-scss": "~5.3.0",
"stylelint-scss": "~4.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"
},
"overrides": {
"ssh2-promise": {
"ssh2": "github:Fabio286/ssh2"
}
} }
} }

View File

@ -63,43 +63,39 @@ async function restartElectron () {
if (!manualRestart) process.exit(0); if (!manualRestart) process.exit(0);
}); });
} }
function startWorkers () {
const compiler = webpack(workersConfig);
const { name } = compiler;
function startMain () { compiler.hooks.afterEmit.tap('afterEmit', () => {
const webpackSetup = webpack([mainConfig, workersConfig]); console.log(chalk.gray(`\nCompiled ${name} script!`));
console.log(chalk.gray(`\nWatching file changes for ${name} script...`));
webpackSetup.compilers.forEach((compiler) => {
const { name } = compiler;
switch (name) {
case 'workers':
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(chalk.gray(`\nCompiled ${name} script!`));
console.log(
chalk.gray(`\nWatching file changes for ${name} script...`)
);
});
break;
case 'main':
default:
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(chalk.gray(`\nCompiled ${name} script!`));
manualRestart = true;
await restartElectron();
setTimeout(() => {
manualRestart = false;
}, 2500);
console.log(
chalk.gray(`\nWatching file changes for ${name} script...`)
);
});
break;
}
}); });
webpackSetup.watch({ aggregateTimeout: 500 }, err => { compiler.watch({ aggregateTimeout: 500 }, err => {
if (err) console.error(chalk.red(err));
});
}
function startMain () {
const compiler = webpack(mainConfig);
const { name } = compiler;
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(chalk.gray(`\nCompiled ${name} script!`));
manualRestart = true;
await restartElectron();
startWorkers();
setTimeout(() => {
manualRestart = false;
}, 2500);
console.log(chalk.gray(`\nWatching file changes for ${name} script...`));
});
compiler.watch({ aggregateTimeout: 500 }, err => {
if (err) console.error(chalk.red(err)); if (err) console.error(chalk.red(err));
}); });
} }
@ -115,6 +111,7 @@ function startRenderer (callback) {
const server = new WebpackDevServer(compiler, { const server = new WebpackDevServer(compiler, {
port: 9080, port: 9080,
hot: true,
client: { client: {
overlay: true, overlay: true,
logging: 'warn' logging: 'warn'

View File

@ -42,6 +42,7 @@ const downloadFile = url => {
await unzip(filePath, destFolder); await unzip(filePath, destFolder);
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
fs.unlinkSync(`${destFolder}/package.json`);// <- Avoid to display annoyng npm script in vscode fs.unlinkSync(`${destFolder}/package.json`);// <- Avoid to display annoyng npm script in vscode
process.exit();
} }
catch (error) { catch (error) {
console.log(error); console.log(error);

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,

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

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,

View File

@ -66,7 +66,7 @@ export default [
group: 'monetary', group: 'monetary',
types: [ types: [
{ {
name: 'money', name: 'MONEY',
length: false, length: false,
unsigned: true unsigned: true
} }

View File

@ -1,3 +1,4 @@
import SSHConfig from '@fabio286/ssh2-promise/lib/sshConfig';
import * as mysql from 'mysql2/promise'; import * as mysql from 'mysql2/promise';
import * as pg from 'pg'; import * as pg from 'pg';
import { FirebirdSQLClient } from 'src/main/libs/clients/FirebirdSQLClient'; import { FirebirdSQLClient } from 'src/main/libs/clients/FirebirdSQLClient';
@ -5,7 +6,6 @@ import MysqlExporter from 'src/main/libs/exporters/sql/MysqlExporter';
import PostgreSQLExporter from 'src/main/libs/exporters/sql/PostgreSQLExporter'; import PostgreSQLExporter from 'src/main/libs/exporters/sql/PostgreSQLExporter';
import MySQLImporter from 'src/main/libs/importers/sql/MySQLlImporter'; import MySQLImporter from 'src/main/libs/importers/sql/MySQLlImporter';
import PostgreSQLImporter from 'src/main/libs/importers/sql/PostgreSQLImporter'; import PostgreSQLImporter from 'src/main/libs/importers/sql/PostgreSQLImporter';
import SSHConfig from 'ssh2-promise/lib/sshConfig';
import { MySQLClient } from '../../main/libs/clients/MySQLClient'; import { MySQLClient } from '../../main/libs/clients/MySQLClient';
import { PostgreSQLClient } from '../../main/libs/clients/PostgreSQLClient'; import { PostgreSQLClient } from '../../main/libs/clients/PostgreSQLClient';
@ -52,6 +52,7 @@ 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;
@ -363,7 +364,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;
@ -44,6 +46,8 @@ export interface Customizations {
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

@ -41,7 +41,7 @@ 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 = /[\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',
@ -153,9 +153,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'};
}) => { }) => {

View File

@ -1,4 +1,4 @@
export const shortcutEvents: { [key: string]: { l18n: string; l18nParam?: string | number; context?: 'tab' }} = { export const shortcutEvents: Record<string, { l18n: string; l18nParam?: string | number; context?: 'tab' }> = {
'run-or-reload': { l18n: 'application.runOrReload', context: 'tab' }, 'run-or-reload': { l18n: 'application.runOrReload', context: 'tab' },
'open-new-tab': { l18n: 'application.openNewTab', context: 'tab' }, 'open-new-tab': { l18n: 'application.openNewTab', context: 'tab' },
'close-tab': { l18n: 'application.closeTab', context: 'tab' }, 'close-tab': { l18n: 'application.closeTab', context: 'tab' },
@ -6,6 +6,9 @@ export const shortcutEvents: { [key: string]: { l18n: string; l18nParam?: string
'kill-query': { l18n: 'database.killQuery', context: 'tab' }, 'kill-query': { l18n: 'database.killQuery', context: 'tab' },
'query-history': { l18n: 'database.queryHistory', context: 'tab' }, 'query-history': { l18n: 'database.queryHistory', context: 'tab' },
'clear-query': { l18n: 'database.clearQuery', context: 'tab' }, 'clear-query': { l18n: 'database.clearQuery', context: 'tab' },
// 'save-file': { l18n: 'application.saveFile', context: 'tab' },
'open-file': { l18n: 'application.openFile', context: 'tab' },
'save-file-as': { l18n: 'application.saveFileAs', context: 'tab' },
'next-tab': { l18n: 'application.nextTab' }, 'next-tab': { l18n: 'application.nextTab' },
'prev-tab': { l18n: 'application.previousTab' }, 'prev-tab': { l18n: 'application.previousTab' },
'open-all-connections': { l18n: 'application.openAllConnections' }, 'open-all-connections': { l18n: 'application.openAllConnections' },
@ -16,7 +19,7 @@ export const shortcutEvents: { [key: string]: { l18n: string; l18nParam?: string
'save-content': { l18n: 'application.saveContent' }, 'save-content': { l18n: 'application.saveContent' },
'create-connection': { l18n: 'connection.createNewConnection' }, 'create-connection': { l18n: 'connection.createNewConnection' },
'open-settings': { l18n: 'application.openSettings' }, 'open-settings': { l18n: 'application.openSettings' },
'open-scratchpad': { l18n: 'application.openScratchpad' } 'open-scratchpad': { l18n: 'application.openNotes' }
}; };
interface ShortcutRecord { interface ShortcutRecord {
@ -119,6 +122,21 @@ 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']
} }
]; ];

View File

@ -1,19 +1,63 @@
import { app, dialog, ipcMain } from 'electron'; import { app, dialog, ipcMain, safeStorage } from 'electron';
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';
export default () => { export default () => {
ipcMain.on('close-app', (event) => { ipcMain.on('close-app', (event) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) {
return {
status: 'error',
response: 'Unauthorized process'
};
}
app.exit(); app.exit();
}); });
ipcMain.on('set-key', (event, key) => {
if (safeStorage.isEncryptionAvailable()) {
const sessionStore = new Store({
name: 'session',
fileExtension: ''
});
const encrypted = safeStorage.encryptString(key);
sessionStore.set('key', encrypted);
event.returnValue = true;
}
});
ipcMain.on('get-key', (event) => {
if (!safeStorage.isEncryptionAvailable()) {
event.returnValue = false;
return;
}
const sessionStore = new Store({
name: 'session',
fileExtension: ''
});
try {
const encrypted = sessionStore.get('key') as string;
const key = safeStorage.decryptString(Buffer.from(encrypted, 'utf-8'));
event.returnValue = key;
}
catch (error) {
event.returnValue = false;
}
});
ipcMain.handle('show-open-dialog', (event, options) => { ipcMain.handle('show-open-dialog', (event, options) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
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');
@ -42,4 +86,26 @@ export default () => {
const shortCutRegister = ShortcutRegister.getInstance(); const shortCutRegister = ShortcutRegister.getInstance();
shortCutRegister.unregister(); shortCutRegister.unregister();
}); });
ipcMain.handle('read-file', (event, filePath) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
const content = fs.readFileSync(filePath, 'utf-8');
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

@ -6,7 +6,7 @@ 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';
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' };
@ -55,7 +55,7 @@ export default (connections: {[key: string]: antares.Client}) => {
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() : null,
passphrase: conn.sshPassphrase, passphrase: conn.sshPassphrase,
keepaliveInterval: conn.sshKeepAliveInterval ?? conn.sshKeepAliveInterval*1000 keepaliveInterval: conn.sshKeepAliveInterval ? conn.sshKeepAliveInterval*1000 : null
}; };
} }
@ -137,7 +137,7 @@ export default (connections: {[key: string]: antares.Client}) => {
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() : null,
passphrase: conn.sshPassphrase, passphrase: conn.sshPassphrase,
keepaliveInterval: conn.sshKeepAliveInterval ?? conn.sshKeepAliveInterval*1000 keepaliveInterval: conn.sshKeepAliveInterval ? conn.sshKeepAliveInterval*1000 : null
}; };
} }
@ -146,7 +146,7 @@ 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();

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

@ -1,17 +1,14 @@
import { ChildProcess, fork } from 'child_process';
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as workers from 'common/interfaces/workers'; import * as workers from 'common/interfaces/workers';
import { dialog, ipcMain } from 'electron'; import { dialog, ipcMain } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import { Worker } from 'worker_threads';
import { validateSender } from '../libs/misc/validateSender'; import { validateSender } from '../libs/misc/validateSender';
const isDevelopment = process.env.NODE_ENV !== 'production'; export default (connections: Record<string, antares.Client>) => {
let exporter: Worker = null;
export default (connections: {[key: string]: antares.Client}) => { let importer: Worker = null;
let exporter: ChildProcess = null;
let importer: ChildProcess = null;
ipcMain.handle('create-schema', async (event, params) => { ipcMain.handle('create-schema', async (event, params) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
@ -202,7 +199,7 @@ 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' };
if (exporter !== null) { if (exporter !== null) {
exporter.kill(); exporter.terminate();
return; return;
} }
@ -227,11 +224,12 @@ export default (connections: {[key: string]: antares.Client}) => {
} }
} }
// Init exporter process // Init exporter thread
exporter = fork(isDevelopment ? './dist/exporter.js' : path.resolve(__dirname, './exporter.js'), [], { // eslint-disable-next-line @typescript-eslint/ban-ts-comment
execArgv: isDevelopment ? ['--inspect=9224'] : undefined // @ts-ignore
}); exporter = new Worker(new URL('../workers/exporter', import.meta.url));
exporter.send({
exporter.postMessage({
type: 'init', type: 'init',
client: { client: {
name: type, name: type,
@ -242,32 +240,34 @@ export default (connections: {[key: string]: antares.Client}) => {
}); });
// Exporter message listener // Exporter message listener
exporter.on('message', ({ type, payload }: workers.WorkerIpcMessage) => { exporter.on('message', (message: workers.WorkerIpcMessage) => {
const { type, payload } = message;
switch (type) { switch (type) {
case 'export-progress': case 'export-progress':
event.sender.send('export-progress', payload); event.sender.send('export-progress', payload);
break; break;
case 'end': case 'end':
setTimeout(() => { // Ensures that writing process has finished setTimeout(() => { // Ensures that writing thread has finished
exporter.kill(); exporter?.terminate();
exporter = null; exporter = null;
}, 2000); }, 2000);
resolve({ status: 'success', response: payload }); resolve({ status: 'success', response: payload });
break; break;
case 'cancel': case 'cancel':
exporter.kill(); exporter?.terminate();
exporter = null; exporter = null;
resolve({ status: 'error', response: 'Operation cancelled' }); resolve({ status: 'error', response: 'Operation cancelled' });
break; break;
case 'error': case 'error':
exporter.kill(); exporter?.terminate();
exporter = null; exporter = null;
resolve({ status: 'error', response: payload }); resolve({ status: 'error', response: payload });
break; break;
} }
}); });
exporter.on('exit', code => { exporter.on('close', code => {
exporter = null; exporter = null;
resolve({ status: 'error', response: `Operation ended with code: ${code}` }); resolve({ status: 'error', response: `Operation ended with code: ${code}` });
}); });
@ -291,7 +291,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (result.response === 1) { if (result.response === 1) {
willAbort = true; willAbort = true;
exporter.send({ type: 'cancel' }); exporter.postMessage({ type: 'cancel' });
} }
} }
@ -302,7 +302,7 @@ 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' };
if (importer !== null) { if (importer !== null) {
importer.kill(); importer.terminate();
return; return;
} }
@ -310,18 +310,21 @@ export default (connections: {[key: string]: antares.Client}) => {
(async () => { (async () => {
const dbConfig = await connections[options.uid].getDbConfig(); const dbConfig = await connections[options.uid].getDbConfig();
// Init importer process // Init importer thread
importer = fork(isDevelopment ? './dist/importer.js' : path.resolve(__dirname, './importer.js'), [], { // eslint-disable-next-line @typescript-eslint/ban-ts-comment
execArgv: isDevelopment ? ['--inspect=9224'] : undefined // @ts-ignore
}); importer = new Worker(new URL('../workers/importer', import.meta.url));
importer.send({
importer.postMessage({
type: 'init', type: 'init',
dbConfig, dbConfig,
options options
}); });
// Importer message listener // Importer message listener
importer.on('message', ({ type, payload }: workers.WorkerIpcMessage) => { importer.on('message', (message: workers.WorkerIpcMessage) => {
const { type, payload } = message;
switch (type) { switch (type) {
case 'import-progress': case 'import-progress':
event.sender.send('import-progress', payload); event.sender.send('import-progress', payload);
@ -331,23 +334,28 @@ export default (connections: {[key: string]: antares.Client}) => {
break; break;
case 'end': case 'end':
setTimeout(() => { // Ensures that writing process has finished setTimeout(() => { // Ensures that writing process has finished
importer?.kill(); importer?.terminate();
importer = null; importer = null;
}, 2000); }, 2000);
resolve({ status: 'success', response: payload }); resolve({ status: 'success', response: payload });
break; break;
case 'cancel': case 'cancel':
importer.kill(); importer.terminate();
importer = null; importer = null;
resolve({ status: 'error', response: 'Operation cancelled' }); resolve({ status: 'error', response: 'Operation cancelled' });
break; break;
case 'error': case 'error':
importer.kill(); importer.terminate();
importer = null; importer = null;
resolve({ status: 'error', response: payload }); resolve({ status: 'error', response: payload });
break; break;
} }
}); });
importer.on('close', code => {
importer = null;
resolve({ status: 'error', response: `Operation ended with code: ${code}` });
});
})(); })();
}); });
}); });
@ -368,7 +376,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (result.response === 1) { if (result.response === 1) {
willAbort = true; willAbort = true;
importer.send({ type: 'cancel' }); importer.postMessage({ type: 'cancel' });
} }
} }

View File

@ -10,7 +10,7 @@ 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' };
@ -249,7 +249,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 +304,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 +367,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 +437,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

@ -1,4 +1,5 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import * as log from 'electron-log/main';
import * as Store from 'electron-store'; import * as Store from 'electron-store';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
@ -59,7 +60,7 @@ export default () => {
mainWindow.reply('update-downloaded'); mainWindow.reply('update-downloaded');
}); });
// autoUpdater.logger = require('electron-log'); log.transports.file.level = 'info';
// autoUpdater.logger.transports.console.format = '{h}:{i}:{s} {text}'; // log.transports.console.format = '{h}:{i}:{s} {text}';
// autoUpdater.logger.transports.file.level = 'info'; autoUpdater.logger = log;
}; };

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,21 @@ 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, this._mainWindow.webContents.send(shortcut.event);
visible: false, if (isDevelopment) console.log('LOCAL EVENT:', shortcut);
click: () => { }
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 +94,11 @@ export class ShortcutRegister {
} }
} }
} }
this._menu.append(new MenuItem({
label: 'Shortcut',
visible: isMenuVisible,
submenu
}));
} }
private setGlobalShortcuts () { private setGlobalShortcuts () {

View File

@ -1,7 +1,7 @@
import * as antares from 'common/interfaces/antares'; 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 from 'ssh2-promise'; import SSH2Promise = require('@fabio286/ssh2-promise');
const queryLogger = ({ sql, cUid }: {sql: string; cUid: string}) => { const queryLogger = ({ sql, cUid }: {sql: string; cUid: string}) => {
// Remove comments, newlines and multiple spaces // Remove comments, newlines and multiple spaces
@ -10,13 +10,13 @@ const queryLogger = ({ sql, cUid }: {sql: string; cUid: string}) => {
const mainWindow = require('electron').webContents.fromId(1); const mainWindow = require('electron').webContents.fromId(1);
mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() }); mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() });
} }
if (process.env.NODE_ENV === 'development') console.log(escapedSql); 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
*/ */
export abstract class AntaresCore { export abstract class BaseClient {
_client: antares.ClientCode; _client: antares.ClientCode;
protected _cUid: string protected _cUid: string
protected _params: mysql.ConnectionOptions | pg.ClientConfig | { databasePath: string; readonly: boolean}; protected _params: mysql.ConnectionOptions | pg.ClientConfig | { databasePath: string; readonly: boolean};
@ -136,7 +136,7 @@ export abstract class AntaresCore {
} }
// 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;
} }
@ -234,6 +234,18 @@ export abstract class AntaresCore {
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

@ -4,16 +4,16 @@ import * as antares from 'common/interfaces/antares';
import * as firebird from 'node-firebird'; import * as firebird from 'node-firebird';
import * as path from 'path'; import * as path from 'path';
import { AntaresCore } from '../AntaresCore'; import { BaseClient } from './BaseClient';
export class FirebirdSQLClient extends AntaresCore { export class FirebirdSQLClient extends BaseClient {
private _schema?: string; private _schema?: string;
private _runningConnections: Map<string, number>; private _runningConnections: Map<string, number>;
private _connectionsToCommit: Map<string, firebird.Transaction>; private _connectionsToCommit: Map<string, firebird.Transaction>;
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',
@ -118,6 +118,10 @@ export class FirebirdSQLClient extends AntaresCore {
return null; return null;
} }
getDatabases (): null[] {
return [];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async getStructure (_schemas: Set<string>) { async getStructure (_schemas: Set<string>) {
interface TableResult { interface TableResult {
@ -157,10 +161,10 @@ export class FirebirdSQLClient extends AntaresCore {
const { rows: tables } = await this.raw<antares.QueryResult<TableResult>>(` const { rows: tables } = await this.raw<antares.QueryResult<TableResult>>(`
SELECT SELECT
rdb$relation_name AS name, rdb$relation_name AS NAME,
rdb$format AS format, rdb$format AS FORMAT,
rdb$description AS description, rdb$description AS DESCRIPTION,
'table' AS type 'table' AS TYPE
FROM RDB$RELATIONS a FROM RDB$RELATIONS a
WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0 WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0
AND RDB$RELATION_TYPE = 0 AND RDB$RELATION_TYPE = 0
@ -168,8 +172,8 @@ export class FirebirdSQLClient extends AntaresCore {
const { rows: views } = await this.raw<antares.QueryResult<TableResult>>(` const { rows: views } = await this.raw<antares.QueryResult<TableResult>>(`
SELECT SELECT
DISTINCT RDB$VIEW_NAME AS name, DISTINCT RDB$VIEW_NAME AS NAME,
'view' AS type 'view' AS TYPE
FROM RDB$VIEW_RELATIONS FROM RDB$VIEW_RELATIONS
`); `);
@ -177,9 +181,9 @@ export class FirebirdSQLClient extends AntaresCore {
const { rows: triggers } = await this.raw<antares.QueryResult<TriggersResult>>(` const { rows: triggers } = await this.raw<antares.QueryResult<TriggersResult>>(`
SELECT SELECT
RDB$TRIGGER_NAME as name, RDB$TRIGGER_NAME as NAME,
RDB$RELATION_NAME as relation, RDB$RELATION_NAME as RELATION,
RDB$TRIGGER_SOURCE as source RDB$TRIGGER_SOURCE as SOURCE
FROM RDB$TRIGGERS FROM RDB$TRIGGERS
WHERE RDB$SYSTEM_FLAG=0 WHERE RDB$SYSTEM_FLAG=0
ORDER BY RDB$TRIGGER_NAME; ORDER BY RDB$TRIGGER_NAME;
@ -208,8 +212,8 @@ export class FirebirdSQLClient extends AntaresCore {
schemaSize += tableSize; schemaSize += tableSize;
return { return {
name: table.NAME.trim(), name: table.NAME?.trim(),
type: table.TYPE.trim(), type: table.TYPE?.trim(),
rows: false, rows: false,
size: false size: false
}; };
@ -218,8 +222,8 @@ export class FirebirdSQLClient extends AntaresCore {
// TRIGGERS // TRIGGERS
const remappedTriggers = triggersArr.map(trigger => { const remappedTriggers = triggersArr.map(trigger => {
return { return {
name: trigger.NAME.trim(), name: trigger.NAME?.trim(),
table: trigger.RELATION.trim(), table: trigger.RELATION?.trim(),
statement: trigger.SOURCE statement: trigger.SOURCE
}; };
}); });
@ -227,7 +231,7 @@ export class FirebirdSQLClient extends AntaresCore {
// PROCEDURES // PROCEDURES
const remappedProcedures = proceduresArr.map(procedure => { const remappedProcedures = proceduresArr.map(procedure => {
return { return {
name: procedure.NAME.trim(), name: procedure.NAME?.trim(),
definer: procedure.DEFINER, definer: procedure.DEFINER,
comment: procedure.COMMENT?.trim() comment: procedure.COMMENT?.trim()
}; };

View File

@ -1,21 +1,22 @@
import SSH2Promise = require('@fabio286/ssh2-promise');
import SSHConfig from '@fabio286/ssh2-promise/lib/sshConfig';
import dataTypes from 'common/data-types/mysql'; 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 { AntaresCore } from '../AntaresCore'; import { BaseClient } from './BaseClient';
import SSH2Promise = require('ssh2-promise');
import SSHConfig from 'ssh2-promise/lib/sshConfig';
export class MySQLClient extends AntaresCore { export class MySQLClient extends BaseClient {
private _schema?: string; private _schema?: string;
private _runningConnections: Map<string, number>; private _runningConnections: Map<string, number>;
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 +59,10 @@ export class MySQLClient extends AntaresCore {
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;
@ -168,7 +173,10 @@ export class MySQLClient extends AntaresCore {
dbConfig.port = tunnel.localPort; dbConfig.port = tunnel.localPort;
} }
catch (err) { catch (err) {
if (this._ssh) this._ssh.close(); if (this._ssh) {
this._ssh.close();
this._ssh.closeTunnel();
}
throw err; throw err;
} }
} }
@ -178,19 +186,45 @@ export class MySQLClient extends AntaresCore {
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(',')}'`);
});
}
} }
destroy () { destroy () {
this._connection.end(); this._connection.end();
clearInterval(this._keepaliveTimer); clearInterval(this._keepaliveTimer);
this._keepaliveTimer = undefined; this._keepaliveTimer = undefined;
if (this._ssh) this._ssh.close(); if (this._ssh) {
this._ssh.close();
this._ssh.closeTunnel();
}
} }
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,
@ -202,17 +236,6 @@ export class MySQLClient extends AntaresCore {
} }
}); });
// 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;
} }
@ -221,6 +244,7 @@ export class MySQLClient extends AntaresCore {
const connection = mysql.createPool({ const connection = mysql.createPool({
...dbConfig, ...dbConfig,
connectionLimit: this._poolSize, connectionLimit: this._poolSize,
enableKeepAlive: true,
typeCast: (field, next) => { typeCast: (field, next) => {
if (field.type === 'DATETIME') if (field.type === 'DATETIME')
return field.string(); return field.string();
@ -229,25 +253,6 @@ export class MySQLClient extends AntaresCore {
} }
}); });
// 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 () => {
await this.keepAlive(); await this.keepAlive();
}, this._keepaliveMs); }, this._keepaliveMs);
@ -255,6 +260,43 @@ export class MySQLClient extends AntaresCore {
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
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();
@ -576,7 +618,7 @@ export class MySQLClient extends AntaresCore {
} }
}) })
.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;
}, {}); }, {});
@ -585,7 +627,7 @@ export class MySQLClient extends AntaresCore {
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;
@ -1642,28 +1684,7 @@ export class MySQLClient extends AntaresCore {
.map(q => q.trim()) .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;
@ -1723,7 +1744,7 @@ export class MySQLClient extends AntaresCore {
}); });
} }
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);
} }
@ -1735,7 +1756,7 @@ export class MySQLClient extends AntaresCore {
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,7 +1774,7 @@ export class MySQLClient extends AntaresCore {
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);
} }
@ -1770,7 +1791,7 @@ export class MySQLClient extends AntaresCore {
}); });
} }
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

@ -1,13 +1,13 @@
import SSH2Promise = require('@fabio286/ssh2-promise');
import SSHConfig from '@fabio286/ssh2-promise/lib/sshConfig';
import dataTypes from 'common/data-types/postgresql'; import dataTypes from 'common/data-types/postgresql';
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as pg from 'pg'; import * as pg from 'pg';
import * as pgAst from 'pgsql-ast-parser'; import * as pgAst from 'pgsql-ast-parser';
import { AntaresCore } from '../AntaresCore';
import SSH2Promise = require('ssh2-promise');
import SSHConfig from 'ssh2-promise/lib/sshConfig';
import { ConnectionOptions } from 'tls'; import { ConnectionOptions } from 'tls';
import { BaseClient } from './BaseClient';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
function pgToString (value: any) { function pgToString (value: any) {
return value.toString(); return value.toString();
@ -81,15 +81,15 @@ type builtinsTypes =
'JSONB' | 'JSONB' |
'REGNAMESPACE' | 'REGNAMESPACE' |
'REGROLE'; 'REGROLE';
export class PostgreSQLClient extends AntaresCore { export class PostgreSQLClient extends BaseClient {
private _schema?: string; private _schema?: string;
private _runningConnections: Map<string, number>; private _runningConnections: Map<string, number>;
private _connectionsToCommit: Map<string, pg.Client | pg.PoolClient>; private _connectionsToCommit: Map<string, pg.Client | pg.PoolClient>;
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',
@ -180,7 +180,10 @@ export class PostgreSQLClient extends AntaresCore {
dbConfig.port = tunnel.localPort; dbConfig.port = tunnel.localPort;
} }
catch (err) { catch (err) {
if (this._ssh) this._ssh.close(); if (this._ssh) {
this._ssh.close();
this._ssh.closeTunnel();
}
throw err; throw err;
} }
} }
@ -207,6 +210,10 @@ export class PostgreSQLClient extends AntaresCore {
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;
} }
@ -229,6 +236,10 @@ export class PostgreSQLClient extends AntaresCore {
await this.keepAlive(); await this.keepAlive();
}, this._keepaliveMs); }, this._keepaliveMs);
connection.on('error', err => { // Intercepts errors and converts to rejections
Promise.reject(err);
});
return connection; return connection;
} }
@ -236,7 +247,10 @@ export class PostgreSQLClient extends AntaresCore {
this._connection.end(); this._connection.end();
clearInterval(this._keepaliveTimer); clearInterval(this._keepaliveTimer);
this._keepaliveTimer = undefined; this._keepaliveTimer = undefined;
if (this._ssh) this._ssh.close(); if (this._ssh) {
this._ssh.close();
this._ssh.closeTunnel();
}
} }
private async keepAlive () { private async keepAlive () {
@ -321,6 +335,19 @@ export class PostgreSQLClient extends AntaresCore {
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;
@ -329,6 +356,14 @@ export class PostgreSQLClient extends AntaresCore {
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,
@ -364,7 +399,11 @@ export class PostgreSQLClient extends AntaresCore {
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,
@ -646,7 +685,7 @@ export class PostgreSQLClient extends AntaresCore {
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',
@ -1042,11 +1081,32 @@ export class PostgreSQLClient extends AntaresCore {
})[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}`;
@ -1056,11 +1116,25 @@ export class PostgreSQLClient extends AntaresCore {
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('.');

View File

@ -3,9 +3,9 @@ import dataTypes from 'common/data-types/sqlite';
import { DATETIME, FLOAT, NUMBER, TIME } from 'common/fieldTypes'; import { DATETIME, FLOAT, NUMBER, TIME } from 'common/fieldTypes';
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import { AntaresCore } from '../AntaresCore'; import { BaseClient } from './BaseClient';
export class SQLiteClient extends AntaresCore { export class SQLiteClient extends BaseClient {
private _schema?: string; private _schema?: string;
private _connectionsToCommit: Map<string, sqlite.Database>; private _connectionsToCommit: Map<string, sqlite.Database>;
protected _connection?: sqlite.Database; protected _connection?: sqlite.Database;
@ -166,7 +166,7 @@ export class SQLiteClient extends AntaresCore {
type: type.trim(), type: type.trim(),
schema: schema, schema: schema,
table: table, table: table,
numPrecision: [...NUMBER, ...FLOAT].includes(type) ? length : null, numLength: [...NUMBER, ...FLOAT].includes(type) ? length : null,
datePrecision: null, datePrecision: null,
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null, charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
nullable: !field.notnull, nullable: !field.notnull,
@ -658,7 +658,7 @@ export class SQLiteClient extends AntaresCore {
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

@ -1,6 +1,5 @@
import * as exporter from 'common/interfaces/exporter'; import * as exporter from 'common/interfaces/exporter';
import { valueToSqlString } from 'common/libs/sqlUtils'; import { valueToSqlString } from 'common/libs/sqlUtils';
import * as mysql from 'mysql2/promise';
import { MySQLClient } from '../../clients/MySQLClient'; import { MySQLClient } from '../../clients/MySQLClient';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
@ -334,12 +333,10 @@ CREATE TABLE \`${view.Name}\`(
} }
async _queryStream (sql: string) { async _queryStream (sql: string) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql); const connection = await this._client.getConnection();
const isPool = 'getConnection' in this._client._connection;
const connection = isPool ? await (this._client._connection as mysql.Pool).getConnection() : this._client._connection;
// 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 as mysql.PoolConnection).release(); const dispose = () => connection.end();
stream.on('end', dispose); stream.on('end', dispose);
stream.on('error', dispose); stream.on('error', dispose);
@ -357,7 +354,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

@ -59,7 +59,7 @@ SET row_security = off;\n\n\n`;
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',
@ -425,7 +425,6 @@ SET row_security = off;\n\n\n`;
} }
async _queryStream (sql: string) { async _queryStream (sql: string) {
if (process.env.NODE_ENV === 'development') console.log('EXPORTER:', sql);
const connection = await this._client.getConnection(); const connection = await this._client.getConnection();
const query = new QueryStream(sql, null); const query = new QueryStream(sql, null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -441,7 +440,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

@ -33,8 +33,8 @@ export default class MySQLImporter extends BaseImporter {
parser.on('error', reject); parser.on('error', reject);
parser.on('close', async () => { parser.on('close', async () => {
console.log('TOTAL QUERIES', queryCount); // console.log('TOTAL QUERIES', queryCount);
console.log('import end'); // console.log('import end');
resolve(); resolve();
}); });

View File

@ -33,8 +33,8 @@ export default class PostgreSQLImporter extends BaseImporter {
parser.on('error', reject); parser.on('error', reject);
parser.on('close', async () => { parser.on('close', async () => {
console.log('TOTAL QUERIES', queryCount); // console.log('TOTAL QUERIES', queryCount);
console.log('import end'); // console.log('import end');
resolve(); resolve();
}); });

View File

@ -6,7 +6,7 @@ const isWindows = process.platform === 'win32';
const indexPath = path.resolve(__dirname, 'index.html').split(path.sep).join('/'); const indexPath = path.resolve(__dirname, 'index.html').split(path.sep).join('/');
export function validateSender (frame: WebFrameMain) { export function validateSender (frame: WebFrameMain) {
if (process.windowsStore) return true; // TEMP HOTFIX if (isWindows) return true; // TEMP HOTFIX
const frameUrl = new URL(frame.url); const frameUrl = new URL(frame.url);
const prefix = isWindows ? 'file:///' : 'file://'; const prefix = isWindows ? 'file:///' : 'file://';
const framePath = frameUrl.href.replace(prefix, ''); const framePath = frameUrl.href.replace(prefix, '');

View File

@ -1,5 +1,6 @@
import * as remoteMain from '@electron/remote/main'; import * as remoteMain from '@electron/remote/main';
import { app, BrowserWindow, ipcMain, nativeImage } from 'electron'; import { app, BrowserWindow, ipcMain, nativeImage, safeStorage } from 'electron';
import * as log from 'electron-log/main';
import * as Store from 'electron-store'; import * as Store from 'electron-store';
import * as windowStateKeeper from 'electron-window-state'; import * as windowStateKeeper from 'electron-window-state';
import * as path from 'path'; import * as path from 'path';
@ -8,6 +9,7 @@ import ipcHandlers from './ipc-handlers';
import { OsMenu, ShortcutRegister } from './libs/ShortcutRegister'; import { OsMenu, ShortcutRegister } from './libs/ShortcutRegister';
Store.initRenderer(); Store.initRenderer();
log.errorHandler.startCatching();
const settingsStore = new Store({ name: 'settings' }); const settingsStore = new Store({ name: 'settings' });
const appTheme = settingsStore.get('application_theme'); const appTheme = settingsStore.get('application_theme');
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
@ -78,80 +80,80 @@ async function createMainWindow () {
return window; return window;
} }
if (!gotTheLock) app.quit(); require('@electron/remote/main').initialize();
else {
require('@electron/remote/main').initialize();
// Initialize ipcHandlers // Initialize ipcHandlers
ipcHandlers(); ipcHandlers();
ipcMain.on('refresh-theme-settings', () => { ipcMain.on('refresh-theme-settings', () => {
const appTheme = settingsStore.get('application_theme'); const appTheme = settingsStore.get('application_theme');
if (isWindows && mainWindow) { if (isWindows && mainWindow) {
mainWindow.setTitleBarOverlay({ mainWindow.setTitleBarOverlay({
color: appTheme === 'dark' ? '#3f3f3f' : '#fff', color: appTheme === 'dark' ? '#3f3f3f' : '#fff',
symbolColor: appTheme === 'dark' ? '#fff' : '#000' symbolColor: appTheme === 'dark' ? '#fff' : '#000'
});
}
});
ipcMain.on('change-window-title', (_, title: string) => {
if (mainWindow) mainWindow.setTitle(title);
});
// quit application when all windows are closed
app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits
if (!isMacOS) app.quit();
});
app.on('activate', async () => {
// on macOS it is common to re-create a window even after all windows have been closed
if (mainWindow === null)
mainWindow = await createMainWindow();
});
// create main BrowserWindow when electron is ready
app.on('ready', async () => {
mainWindowState = windowStateKeeper({
defaultWidth: 1024,
defaultHeight: 800
}); });
}
});
ipcMain.on('change-window-title', (_, title: string) => {
if (mainWindow) mainWindow.setTitle(title);
});
// quit application when all windows are closed
app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits
if (!isMacOS) app.quit();
});
app.on('activate', async () => {
// on macOS it is common to re-create a window even after all windows have been closed
if (mainWindow === null)
mainWindow = await createMainWindow(); mainWindow = await createMainWindow();
createAppMenu(); });
if (isWindows) // create main BrowserWindow when electron is ready
mainWindow.show(); app.on('ready', async () => {
if (!gotTheLock && !safeStorage.isEncryptionAvailable()) // Disable multiple instances if is not possible to share session keys
app.quit();
// if (isDevelopment) mainWindowState = windowStateKeeper({
// mainWindow.webContents.openDevTools(); defaultWidth: 1024,
defaultHeight: 800
process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error);
});
process.on('unhandledRejection', error => {
mainWindow.webContents.send('unhandled-exception', error);
});
}); });
app.on('browser-window-created', (event, window) => { mainWindow = await createMainWindow();
if (isDevelopment) { createAppMenu();
const { antares } = require('../../package.json');
const extensionPath = path.resolve(__dirname, `../../misc/${antares.devtoolsId}`);
window.webContents.session.loadExtension(extensionPath, { allowFileAccess: true }).catch(console.error);
}
window.webContents.on('will-navigate', (e) => { // Prevent browser navigation if (isWindows)
e.preventDefault(); mainWindow.show();
});
window.webContents.on('did-create-window', (w) => { // Close new windows if (isDevelopment && !isWindows)// Because on Windows you can open devtools from title-bar
w.close(); mainWindow.webContents.openDevTools();
});
process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error);
}); });
}
process.on('unhandledRejection', error => {
mainWindow.webContents.send('unhandled-exception', error);
});
});
app.on('browser-window-created', (event, window) => {
if (isDevelopment) {
const { antares } = require('../../package.json');
const extensionPath = path.resolve(__dirname, `../../misc/${antares.devtoolsId}`);
window.webContents.session.loadExtension(extensionPath, { allowFileAccess: true }).catch(console.error);
}
window.webContents.on('will-navigate', (e) => { // Prevent browser navigation
e.preventDefault();
});
window.webContents.on('did-create-window', (w) => { // Close new windows
w.close();
});
});
function createAppMenu () { function createAppMenu () {
const menuTemplate: OsMenu = { const menuTemplate: OsMenu = {

View File

@ -1,5 +1,7 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as log from 'electron-log/main';
import * as fs from 'fs'; import * as fs from 'fs';
import { parentPort } from 'worker_threads';
import { MySQLClient } from '../libs/clients/MySQLClient'; import { MySQLClient } from '../libs/clients/MySQLClient';
import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient'; import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient';
@ -8,63 +10,78 @@ import MysqlExporter from '../libs/exporters/sql/MysqlExporter';
import PostgreSQLExporter from '../libs/exporters/sql/PostgreSQLExporter'; import PostgreSQLExporter from '../libs/exporters/sql/PostgreSQLExporter';
let exporter: antares.Exporter; let exporter: antares.Exporter;
process.on('message', async ({ type, client, tables, options }: any) => { log.transports.file.fileName = 'workers.log';
log.transports.console = null;
log.errorHandler.startCatching();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const exportHandler = async (data: any) => {
const { type, client, tables, options } = data;
if (type === 'init') { if (type === 'init') {
const connection = await ClientsFactory.getClient({ try {
client: client.name, const connection = await ClientsFactory.getClient({
params: client.config, client: client.name,
poolSize: 5 params: client.config,
}) as MySQLClient | PostgreSQLClient; poolSize: 5
await connection.connect(); }) as MySQLClient | PostgreSQLClient;
await connection.connect();
switch (client.name) { switch (client.name) {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
exporter = new MysqlExporter(connection as MySQLClient, tables, options); exporter = new MysqlExporter(connection as MySQLClient, tables, options);
break; break;
case 'pg': case 'pg':
exporter = new PostgreSQLExporter(connection as PostgreSQLClient, tables, options); exporter = new PostgreSQLExporter(connection as PostgreSQLClient, tables, options);
break; break;
default: default:
process.send({ parentPort.postMessage({
type: 'error',
payload: `"${client.name}" exporter not aviable`
});
return;
}
exporter.once('error', err => {
log.error(err.toString());
parentPort.postMessage({
type: 'error', type: 'error',
payload: `"${client.name}" exporter not aviable` payload: err.toString()
}); });
return; });
}
exporter.once('error', err => { exporter.once('end', () => {
console.error(err); parentPort.postMessage({
process.send({ type: 'end',
payload: { cancelled: exporter.isCancelled }
});
});
exporter.once('cancel', () => {
fs.unlinkSync(exporter.outputFile);
parentPort.postMessage({ type: 'cancel' });
});
exporter.on('progress', state => {
parentPort.postMessage({
type: 'export-progress',
payload: state
});
});
exporter.run();
}
catch (err) {
log.error(err.toString());
parentPort.postMessage({
type: 'error', type: 'error',
payload: err.toString() payload: err.toString()
}); });
}); }
exporter.once('end', () => {
process.send({
type: 'end',
payload: { cancelled: exporter.isCancelled }
});
connection.destroy();
});
exporter.once('cancel', () => {
fs.unlinkSync(exporter.outputFile);
process.send({ type: 'cancel' });
});
exporter.on('progress', state => {
process.send({
type: 'export-progress',
payload: state
});
});
exporter.run();
} }
else if (type === 'cancel') else if (type === 'cancel')
exporter.cancel(); exporter.cancel();
}); };
process.on('beforeExit', console.log); parentPort.on('message', exportHandler);

View File

@ -1,8 +1,10 @@
import SSHConfig from '@fabio286/ssh2-promise/lib/sshConfig';
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import { ImportOptions } from 'common/interfaces/importer'; import { ImportOptions } from 'common/interfaces/importer';
import * as log from 'electron-log/main';
import * as mysql from 'mysql2'; import * as mysql from 'mysql2';
import * as pg from 'pg'; import * as pg from 'pg';
import SSHConfig from 'ssh2-promise/lib/sshConfig'; import { parentPort } from 'worker_threads';
import { MySQLClient } from '../libs/clients/MySQLClient'; import { MySQLClient } from '../libs/clients/MySQLClient';
import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient'; import { PostgreSQLClient } from '../libs/clients/PostgreSQLClient';
@ -11,14 +13,18 @@ import MySQLImporter from '../libs/importers/sql/MySQLlImporter';
import PostgreSQLImporter from '../libs/importers/sql/PostgreSQLImporter'; import PostgreSQLImporter from '../libs/importers/sql/PostgreSQLImporter';
let importer: antares.Importer; let importer: antares.Importer;
// eslint-disable-next-line @typescript-eslint/no-explicit-any log.transports.file.fileName = 'workers.log';
process.on('message', async ({ type, dbConfig, options }: { log.transports.console = null;
log.errorHandler.startCatching();
const importHandler = async (data: {
type: string; type: string;
dbConfig: mysql.ConnectionOptions & { schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean } dbConfig: mysql.ConnectionOptions & { schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean }
| pg.ClientConfig & { schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean } | pg.ClientConfig & { schema: string; ssl?: mysql.SslOptions; ssh?: SSHConfig; readonly: boolean }
| { databasePath: string; readonly: boolean }; | { databasePath: string; readonly: boolean };
options: ImportOptions; options: ImportOptions;
}) => { }) => {
const { type, dbConfig, options } = data;
if (type === 'init') { if (type === 'init') {
try { try {
const connection = await ClientsFactory.getClient({ const connection = await ClientsFactory.getClient({
@ -41,7 +47,7 @@ process.on('message', async ({ type, dbConfig, options }: {
importer = new PostgreSQLImporter(pool as unknown as pg.PoolClient, options); importer = new PostgreSQLImporter(pool as unknown as pg.PoolClient, options);
break; break;
default: default:
process.send({ parentPort.postMessage({
type: 'error', type: 'error',
payload: `"${options.type}" importer not aviable` payload: `"${options.type}" importer not aviable`
}); });
@ -49,33 +55,33 @@ process.on('message', async ({ type, dbConfig, options }: {
} }
importer.once('error', err => { importer.once('error', err => {
console.error(err); log.error(err.toString());
process.send({ parentPort.postMessage({
type: 'error', type: 'error',
payload: err.toString() payload: err.toString()
}); });
}); });
importer.once('end', () => { importer.once('end', () => {
process.send({ parentPort.postMessage({
type: 'end', type: 'end',
payload: { cancelled: importer.isCancelled } payload: { cancelled: importer.isCancelled }
}); });
}); });
importer.once('cancel', () => { importer.once('cancel', () => {
process.send({ type: 'cancel' }); parentPort.postMessage({ type: 'cancel' });
}); });
importer.on('progress', state => { importer.on('progress', state => {
process.send({ parentPort.postMessage({
type: 'import-progress', type: 'import-progress',
payload: state payload: state
}); });
}); });
importer.on('query-error', state => { importer.on('query-error', state => {
process.send({ parentPort.postMessage({
type: 'query-error', type: 'query-error',
payload: state payload: state
}); });
@ -84,8 +90,8 @@ process.on('message', async ({ type, dbConfig, options }: {
importer.run(); importer.run();
} }
catch (err) { catch (err) {
console.error(err); log.error(err.toString());
process.send({ parentPort.postMessage({
type: 'error', type: 'error',
payload: err.toString() payload: err.toString()
}); });
@ -93,20 +99,6 @@ process.on('message', async ({ type, dbConfig, options }: {
} }
else if (type === 'cancel') else if (type === 'cancel')
importer.cancel(); importer.cancel();
}); };
process.on('uncaughtException', (err) => { parentPort.on('message', importHandler);
console.error(err);
process.send({
type: 'error',
payload: err.toString()
});
});
process.on('unhandledRejection', (err) => {
console.error(err);
process.send({
type: 'error',
payload: err.toString()
});
});

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

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

@ -280,7 +280,6 @@ export default defineComponent({
if (props.searchable) if (props.searchable)
searchInput.value.focus(); searchInput.value.focus();
else else
el.value.focus(); el.value.focus();

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

@ -54,7 +54,7 @@ const updateWindow = () => {
const totalScrollHeight = props.items.length * props.itemHeight; const totalScrollHeight = props.items.length * props.itemHeight;
const offset = 50; const offset = 50;
const scrollTop = localScrollElement.value.scrollTop; const scrollTop = localScrollElement.value?.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight); const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount; const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;

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

View File

@ -152,6 +152,14 @@
/> />
SSH SSH
</div> </div>
<div v-if="connection.readonly" class="chip bg-success mt-2">
<BaseIcon
icon-name="mdiLock"
class="mr-1"
:size="18"
/>
Read-only
</div>
</div> </div>
</div> </div>
</div> </div>
@ -352,7 +360,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

@ -204,7 +204,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

@ -327,7 +327,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'

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

View File

@ -75,7 +75,7 @@
<code <code
class="cut-text" class="cut-text"
:title="query.sql" :title="query.sql"
v-html="highlightWord(query.sql)" v-html="highlight(highlightWord(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>
@ -126,13 +126,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue'; import { highlight } from 'sql-highlight';
import {
Component,
computed,
ComputedRef,
onBeforeUnmount,
onMounted,
onUpdated,
Prop,
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 BaseVirtualScroll from '@/components/BaseVirtualScroll.vue'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import { useFilters } from '@/composables/useFilters'; import { useFilters } from '@/composables/useFilters';
import { useFocusTrap } from '@/composables/useFocusTrap'; import { useFocusTrap } from '@/composables/useFocusTrap';
import { copyText } from '@/libs/copyText';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { HistoryRecord, useHistoryStore } from '@/stores/history'; import { HistoryRecord, useHistoryStore } from '@/stores/history';
@ -162,7 +175,7 @@ const localSearchTerm = ref('');
const connectionName = computed(() => getConnectionName(props.connection.uid)); const connectionName = computed(() => getConnectionName(props.connection.uid));
const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || [])); const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || []));
const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(searchTerm.value.toLowerCase()) >= 0)); const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(localSearchTerm.value.toLowerCase()) >= 0));
watch(searchTerm, () => { watch(searchTerm, () => {
clearTimeout(searchTermInterval.value); clearTimeout(searchTermInterval.value);
@ -173,7 +186,7 @@ watch(searchTerm, () => {
}); });
const copyQuery = (sql: string) => { const copyQuery = (sql: string) => {
navigator.clipboard.writeText(sql); copyText(sql);
}; };
const deleteQuery = (query: HistoryRecord[]) => { const deleteQuery = (query: HistoryRecord[]) => {
@ -274,7 +287,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

@ -0,0 +1,124 @@
<template>
<ConfirmModal
size="resize"
:disable-autofocus="true"
:close-on-confirm="!!localNote.note.length"
:confirm-text="t('general.save')"
@confirm="updateNote"
@hide="$emit('hide')"
>
<template #header>
<div class="d-flex">
<BaseIcon
icon-name="mdiNoteEditOutline"
class="mr-1"
:size="24"
/> {{ t('application.editNote') }}
</div>
</template>
<template #body>
<form class="form">
<div class="form-group columns">
<div class="column col-8">
<label class="form-label">{{ t('connection.connection') }}</label>
<BaseSelect
v-model="localNote.cUid"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
<div class="column col-4">
<label class="form-label">{{ t('application.tag') }}</label>
<BaseSelect
v-model="localNote.type"
class="form-select"
:options="noteTags"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
</div>
<div class="form-group">
<label class="form-label">{{ t('general.content') }} <small
v-if="localNote.type !== 'query'"
style="line-height: 1;"
class="text-gray"
>({{ t('application.markdownSupported') }})</small></label>
<BaseTextEditor
v-model="localNote.note"
:mode="editorMode"
:show-line-numbers="false"
:auto-focus="true"
:height="400"
:width="640"
:resizable="true"
/>
</div>
</form>
</template>
</ConfirmModal>
</template>
<script lang="ts" setup>
import { inject, onBeforeMount, PropType, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import BaseTextEditor from '@/components/BaseTextEditor.vue';
import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad';
const { t } = useI18n();
const { editNote } = useScratchpadStore();
const emit = defineEmits(['hide']);
const props = defineProps({
note: {
type: Object as PropType<ConnectionNote>,
required: true
}
});
const noteTags = inject<{code: TagCode; name: string}[]>('noteTags');
const connectionOptions = inject<{code: string; name: string}[]>('connectionOptions');
const editorMode = ref('markdown');
const localNote: Ref<ConnectionNote> = ref({
uid: 'dummy',
cUid: null,
title: undefined,
note: '',
date: new Date(),
type: 'note',
isArchived: false
});
const updateNote = () => {
if (localNote.value.note) {
if (!localNote.value.title)// Set a default title
localNote.value.title = `${localNote.value.type.toLocaleUpperCase()}: ${localNote.value.uid}`;
localNote.value.date = new Date();
editNote(localNote.value);
emit('hide');
}
};
watch(() => localNote.value.type, () => {
if (localNote.value.type === 'query')
editorMode.value = 'sql';
else
editorMode.value = 'markdown';
});
onBeforeMount(() => {
localNote.value = JSON.parse(JSON.stringify(props.note));
});
</script>

View File

@ -0,0 +1,122 @@
<template>
<ConfirmModal
size="resize"
:disable-autofocus="true"
:close-on-confirm="!!newNote.note.length"
:confirm-text="t('general.save')"
@confirm="createNote"
@hide="$emit('hide')"
>
<template #header>
<div class="d-flex">
<BaseIcon
icon-name="mdiNotePlusOutline"
class="mr-1"
:size="24"
/> {{ t('application.addNote') }}
</div>
</template>
<template #body>
<form class="form">
<div class="form-group columns">
<div class="column col-8">
<label class="form-label">{{ t('connection.connection') }}</label>
<BaseSelect
v-model="newNote.cUid"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
<div class="column col-4">
<label class="form-label">{{ t('application.tag') }}</label>
<BaseSelect
v-model="newNote.type"
class="form-select"
:options="noteTags"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
</div>
<div class="form-group">
<label class="form-label">{{ t('general.content') }} <small
v-if="newNote.type !== 'query'"
style="line-height: 1;"
class="text-gray"
>({{ t('application.markdownSupported') }})</small></label>
<BaseTextEditor
v-model="newNote.note"
:mode="editorMode"
:show-line-numbers="false"
:auto-focus="true"
:height="400"
:width="640"
:resizable="true"
/>
</div>
</form>
</template>
</ConfirmModal>
</template>
<script lang="ts" setup>
import { uidGen } from 'common/libs/uidGen';
import { inject, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import BaseTextEditor from '@/components/BaseTextEditor.vue';
import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad';
const { t } = useI18n();
const { addNote } = useScratchpadStore();
const emit = defineEmits(['hide']);
const noteTags = inject<{code: TagCode; name: string}[]>('noteTags');
const selectedConnection = inject<Ref<null | string>>('selectedConnection');
const selectedTag = inject<Ref<TagCode>>('selectedTag');
const connectionOptions = inject<{code: string; name: string}[]>('connectionOptions');
const editorMode = ref('markdown');
const newNote: Ref<ConnectionNote> = ref({
uid: uidGen('N'),
cUid: null,
title: undefined,
note: '',
date: new Date(),
type: 'note',
isArchived: false
});
const createNote = () => {
if (newNote.value.note) {
if (!newNote.value.title)// Set a default title
newNote.value.title = `${newNote.value.type.toLocaleUpperCase()}: ${newNote.value.uid}`;
newNote.value.date = new Date();
addNote(newNote.value);
emit('hide');
}
};
watch(() => newNote.value.type, () => {
if (newNote.value.type === 'query')
editorMode.value = 'sql';
else
editorMode.value = 'markdown';
});
newNote.value.cUid = selectedConnection.value;
if (selectedTag.value !== 'all')
newNote.value.type = selectedTag.value;
</script>

View File

@ -161,6 +161,7 @@ import ModalProcessesListContext from '@/components/ModalProcessesListContext.vu
import ModalProcessesListRow from '@/components/ModalProcessesListRow.vue'; import ModalProcessesListRow from '@/components/ModalProcessesListRow.vue';
import { useFocusTrap } from '@/composables/useFocusTrap'; import { useFocusTrap } from '@/composables/useFocusTrap';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { copyText } from '@/libs/copyText';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
@ -322,13 +323,13 @@ const closeContext = () => {
const copyCell = () => { const copyCell = () => {
const row = results.value.find(row => Number(row.id) === selectedRow.value); const row = results.value.find(row => Number(row.id) === selectedRow.value);
const valueToCopy = row[selectedCell.value.field]; const valueToCopy = row[selectedCell.value.field];
navigator.clipboard.writeText(valueToCopy); copyText(valueToCopy);
}; };
const copyRow = () => { const copyRow = () => {
const row = results.value.find(row => Number(row.id) === selectedRow.value); const row = results.value.find(row => Number(row.id) === selectedRow.value);
const rowToCopy = JSON.parse(JSON.stringify(row)); const rowToCopy = JSON.parse(JSON.stringify(row));
navigator.clipboard.writeText(JSON.stringify(rowToCopy)); copyText(JSON.stringify(rowToCopy));
}; };
const closeModal = () => emit('close'); const closeModal = () => emit('close');

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

@ -166,19 +166,6 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ t('application.disableScratchpad') }}
</label>
</div>
<div class="col-3 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleDisableScratchpad">
<input type="checkbox" :checked="disableScratchpad">
<i class="form-icon" />
</label>
</div>
</div>
<div class="form-group column col-12"> <div class="form-group column col-12">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
@ -422,14 +409,6 @@
class="d-inline mr-1" class="d-inline mr-1"
:size="16" :size="16"
/> Mastodon</a> <a /> Mastodon</a> <a
class="c-hand"
:style="'align-items: center; display: inline-flex;'"
@click="openOutside('https://twitter.com/AntaresSQL')"
><BaseIcon
icon-name="mdiTwitter"
class="d-inline mr-1"
:size="16"
/> Twitter</a> <a
class="c-hand" class="c-hand"
:style="'align-items: center; display: inline-flex;'" :style="'align-items: center; display: inline-flex;'"
@click="openOutside('https://antares-sql.app/')" @click="openOutside('https://antares-sql.app/')"
@ -499,7 +478,6 @@ const {
restoreTabs, restoreTabs,
showTableSize, showTableSize,
disableBlur, disableBlur,
disableScratchpad,
applicationTheme, applicationTheme,
editorTheme, editorTheme,
editorFontSize editorFontSize
@ -512,7 +490,6 @@ const {
changePageSize, changePageSize,
changeRestoreTabs, changeRestoreTabs,
changeDisableBlur, changeDisableBlur,
changeDisableScratchpad,
changeAutoComplete, changeAutoComplete,
changeLineWrap, changeLineWrap,
changeExecuteSelected, changeExecuteSelected,
@ -671,10 +648,6 @@ const toggleDisableBlur = () => {
changeDisableBlur(!disableBlur.value); changeDisableBlur(!disableBlur.value);
}; };
const toggleDisableScratchpad = () => {
changeDisableScratchpad(!disableScratchpad.value);
};
const toggleAutoComplete = () => { const toggleAutoComplete = () => {
changeAutoComplete(!selectedAutoComplete.value); changeAutoComplete(!selectedAutoComplete.value);
}; };
@ -730,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);
} }
} }
@ -758,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

@ -409,7 +409,7 @@ defineExpose({ editor });
position: absolute; position: absolute;
left: 3px; left: 3px;
top: 2px; top: 2px;
color: $primary-color; color: var(--primary-color);
display: inline-block; display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons", sans-serif; font: normal normal normal 24px/1 "Material Design Icons", sans-serif;
font-size: inherit; font-size: inherit;

View File

@ -0,0 +1,352 @@
<template>
<div
class="tile my-2"
tabindex="0"
@click="$emit('select-note', note.uid)"
>
<BaseIcon
v-if="isBig"
class="tile-compress c-hand"
:icon-name="isSelected ? 'mdiChevronUp' : 'mdiChevronDown'"
:size="36"
@click.stop="$emit('toggle-note', note.uid)"
/>
<div class="tile-icon">
<BaseIcon
:icon-name="note.type === 'query'
? 'mdiHeartOutline'
: note.type === 'todo'
? note.isArchived
? 'mdiCheckboxMarkedOutline'
: 'mdiCheckboxBlankOutline'
: 'mdiNoteEditOutline'"
:size="36"
/>
<div class="tile-icon-type">
{{ note.type }}
</div>
</div>
<div class="tile-content">
<div class="tile-content-message" :class="[{'opened': isSelected}]">
<code
v-if="note.type === 'query'"
ref="noteParagraph"
class="tile-paragraph sql"
v-html="highlight(highlightWord(note.note), {html: true})"
/>
<div
v-else
ref="noteParagraph"
class="tile-paragraph"
v-html="parseMarkdown(highlightWord(note.note))"
/>
<div v-if="isBig && !isSelected" class="tile-paragraph-overlay" />
</div>
<div class="tile-bottom-content">
<small class="tile-subtitle">{{ getConnectionName(note.cUid) || t('general.all') }} · {{ formatDate(note.date) }}</small>
<div class="tile-history-buttons">
<button
v-if="note.type === 'todo' && !note.isArchived"
class="btn btn-link pl-1"
@click.stop="$emit('archive-note', note.uid)"
>
<BaseIcon
icon-name="mdiCheck"
class="pr-1"
:size="22"
/> {{ t('general.archive') }}
</button>
<button
v-if="note.type === 'todo' && note.isArchived"
class="btn btn-link pl-1"
@click.stop="$emit('restore-note', note.uid)"
>
<BaseIcon
icon-name="mdiRestore"
class="pr-1"
:size="22"
/> {{ t('general.undo') }}
</button>
<button
v-if="note.type === 'query'"
class="btn btn-link pl-1"
@click.stop="$emit('select-query', note.note)"
>
<BaseIcon
icon-name="mdiOpenInApp"
class="pr-1"
:size="22"
/> {{ t('general.select') }}
</button>
<button
v-if="note.type === 'query'"
class="btn btn-link pl-1"
@click.stop="copyText(note.note)"
>
<BaseIcon
icon-name="mdiContentCopy"
class="pr-1"
:size="22"
/> {{ t('general.copy') }}
</button>
<button
v-if=" !note.isArchived"
class="btn btn-link pl-1"
@click.stop="$emit('edit-note')"
>
<BaseIcon
icon-name="mdiPencil"
class="pr-1"
:size="22"
/> {{ t('general.edit') }}
</button>
<button class="btn btn-link pl-1" @click.stop="$emit('delete-note', note.uid)">
<BaseIcon
icon-name="mdiDeleteForever"
class="pr-1"
:size="22"
/> {{ t('general.delete') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useElementBounding } from '@vueuse/core';
import { marked } from 'marked';
import { highlight } from 'sql-highlight';
import { computed, PropType, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import { useFilters } from '@/composables/useFilters';
import { copyText } from '@/libs/copyText';
import { useConnectionsStore } from '@/stores/connections';
import { ConnectionNote } from '@/stores/scratchpad';
const props = defineProps({
note: {
type: Object as PropType<ConnectionNote>,
required: true
},
searchTerm: {
type: String,
default: () => ''
},
selectedNote: {
type: String,
default: () => ''
}
});
const { t } = useI18n();
const { formatDate } = useFilters();
const { getConnectionName } = useConnectionsStore();
defineEmits([
'edit-note',
'delete-note',
'select-note',
'toggle-note',
'archive-note',
'restore-note',
'select-query'
]);
const noteParagraph: Ref<HTMLDivElement> = ref(null);
const noteHeight = ref(useElementBounding(noteParagraph)?.height);
const isSelected = computed(() => props.selectedNote === props.note.uid);
const isBig = computed(() => noteHeight.value > 75);
const parseMarkdown = (text: string) => {
const renderer = {
listitem (text: string) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
},
link (href: string, title: string, text: string) {
return `<a>${text}</a>`;
}
};
marked.use({ renderer });
return marked(text);
};
const highlightWord = (string: string) => {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (props.searchTerm) {
const regexp = new RegExp(`(${props.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
}
else
return string;
};
</script>
<style scoped lang="scss">
.tile {
border-radius: $border-radius;
display: flex;
position: relative;
transition: none;
&:hover,
&:focus {
.tile-content {
.tile-bottom-content {
.tile-history-buttons {
opacity: 1;
}
}
}
}
.tile-compress {
position: absolute;
right: 2px;
top: 0px;
opacity: .7;
z-index: 2;
}
.tile-icon {
font-size: 1.2rem;
margin-left: 0.3rem;
margin-right: 0.3rem;
margin-top: 0.6rem;
width: 40px;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
opacity: .8;
.tile-icon-type {
text-transform: uppercase;
font-size: .5rem;
}
}
.tile-content {
padding: 0.3rem;
padding-left: 0.1rem;
min-height: 75px;
display: flex;
flex-direction: column;
justify-content: space-between;
.tile-content-message{
position: relative;
&:not(.opened) {
max-height: 36px;
overflow: hidden;
}
.tile-paragraph-overlay {
height: 36px;
width: 100%;
position: absolute;
top: 0;
}
}
code, pre {
max-width: 100%;
width: 100%;
display: inline-block;
font-size: 100%;
opacity: 0.8;
font-weight: 600;
white-space: break-spaces;
}
.tile-subtitle {
opacity: 0.8;
}
.tile-bottom-content {
display: flex;
justify-content: space-between;
.tile-history-buttons {
opacity: 0;
transition: opacity 0.2s;
button {
font-size: 0.7rem;
height: 1rem;
line-height: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
}
}
}
}
.theme-dark {
.tile {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
$body-bg-dark);
}
&:focus {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0)70%,
#323232);
}
}
&:hover{
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
$bg-color-light-dark);
}
}
}
}
.theme-light {
.tile {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
#FFFF);
}
&:hover,
&:focus {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
$bg-color-light-gray);
}
}
}
}
</style>
<style lang="scss">
.tile-paragraph {
white-space: initial;
word-break: break-word;
user-select: text;
h1, h2, h3, h4, h5, h6, p, li {
margin: 0;
}
}
</style>

View File

@ -15,6 +15,64 @@
: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">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiBrushVariant"
:size="18"
/> {{ t('application.appearance') }}</span>
</div>
<div <div
v-if="!contextConnection.isFolder" v-if="!contextConnection.isFolder"
class="context-element" class="context-element"
@ -27,14 +85,6 @@
:size="18" :size="18"
/> {{ t('general.duplicate') }}</span> /> {{ t('general.duplicate') }}</span>
</div> </div>
<div class="context-element" @click.stop="showAppearanceModal">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiBrushVariant"
:size="18"
/> {{ t('application.appearance') }}</span>
</div>
<div class="context-element" @click="showConfirmModal"> <div class="context-element" @click="showConfirmModal">
<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">
@ -85,10 +85,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 +118,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 +134,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

@ -1,66 +1,368 @@
<template> <template>
<ConfirmModal <Teleport to="#window-content">
:confirm-text="t('application.update')" <div class="modal active">
:cancel-text="t('general.close')" <a class="modal-overlay" @click.stop="hideScratchpad" />
size="large" <div ref="trapRef" class="modal-container p-0 pb-4">
:hide-footer="true" <div class="modal-header pl-2">
@hide="hideScratchpad" <div class="modal-title h6">
> <div class="d-flex">
<template #header> <BaseIcon
<div class="d-flex"> icon-name="mdiNotebookOutline"
<BaseIcon class="mr-1"
icon-name="mdiNotebookEditOutline" :size="24"
class="mr-1" />
:size="24" <span>{{ t('application.note', 2) }}</span>
/> {{ t('application.scratchpad') }} </div>
</div> </div>
</template> <a class="btn btn-clear c-hand" @click.stop="hideScratchpad" />
<template #body> </div>
<div> <div class="modal-body p-0 workspace-query-results">
<div> <div
<TextEditor ref="noteFilters"
v-model="localNotes" class="d-flex p-vcentered p-2"
editor-class="textarea-editor" style="gap: 0 10px"
mode="markdown" >
:auto-focus="true" <div style="flex: 1;">
:show-line-numbers="false" <BaseSelect
/> v-model="localConnection"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
<div class="btn-group btn-group-block text-uppercase">
<div
v-for="tag in [{ code: 'all', name: t('general.all') }, ...noteTags]"
:key="tag.code"
class="btn"
:class="[selectedTag === tag.code ? 'btn-primary': 'btn-dark']"
@click="setTag(tag.code)"
>
{{ tag.name }}
</div>
</div>
<div class="">
<div
class="btn px-1 tooltip tooltip-left s-rounded archived-button"
:class="[showArchived ? 'btn-primary' : 'btn-link']"
:data-tooltip="showArchived ? t('application.hideArchivedNotes') : t('application.showArchivedNotes')"
@click="showArchived = !showArchived"
>
<BaseIcon
:icon-name="!showArchived ? 'mdiArchiveEyeOutline' : 'mdiArchiveCancelOutline'"
class=""
:size="24"
/>
</div>
</div>
</div>
<div>
<div
v-show="filteredNotes.length || searchTerm.length"
ref="searchForm"
class="form-group has-icon-right m-0 p-2"
>
<input
v-model="searchTerm"
class="form-input"
type="text"
:placeholder="t('general.search')"
>
<BaseIcon
v-if="!searchTerm"
icon-name="mdiMagnify"
class="form-icon pr-2"
:size="18"
/>
<BaseIcon
v-else
icon-name="mdiBackspace"
class="form-icon c-hand pr-2"
:size="18"
@click="searchTerm = ''"
/>
</div>
</div>
<div
v-if="filteredNotes.length"
ref="tableWrapper"
class="vscroll px-2"
:style="{'height': resultsSize+'px'}"
>
<div ref="table">
<BaseVirtualScroll
ref="resultTable"
:items="filteredNotes"
:item-height="83"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template #default="{ items }">
<ScratchpadNote
v-for="note in items"
:key="note.uid"
:search-term="searchTerm"
:note="note"
:selected-note="selectedNote"
@select-note="selectedNote = note.uid"
@toggle-note="toggleNote"
@edit-note="startEditNote(note)"
@delete-note="deleteNote"
@archive-note="archiveNote"
@restore-note="restoreNote"
@select-query="selectQuery"
/>
</template>
</BaseVirtualScroll>
</div>
</div>
<div v-else class="empty">
<div class="empty-icon">
<BaseIcon icon-name="mdiNoteSearch" :size="48" />
</div>
<p class="empty-title h5">
{{ t('application.thereAreNoNotesYet') }}
</p>
</div>
<div
class="btn btn-primary p-0 add-button tooltip tooltip-left"
:data-tooltip="t('application.addNote')"
@click="isAddModal = true"
>
<BaseIcon
icon-name="mdiPlus"
:size="48"
/>
</div>
</div> </div>
<small class="text-gray">{{ t('application.markdownSupported') }}</small>
</div> </div>
</template> </div>
</ConfirmModal> </Teleport>
<ModalNoteNew v-if="isAddModal" @hide="isAddModal = false" />
<ModalNoteEdit
v-if="isEditModal"
:note="noteToEdit"
@hide="closeEditModal"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Ref, ref, watch } from 'vue'; import {
Component,
computed,
ComputedRef,
onBeforeUnmount,
onMounted,
onUpdated,
provide,
Ref,
ref,
watch
} from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import TextEditor from '@/components/BaseTextEditor.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import ModalNoteEdit from '@/components/ModalNoteEdit.vue';
import ModalNoteNew from '@/components/ModalNoteNew.vue';
import ScratchpadNote from '@/components/ScratchpadNote.vue';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useScratchpadStore } from '@/stores/scratchpad'; import { useConnectionsStore } from '@/stores/connections';
import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad';
import { useWorkspacesStore } from '@/stores/workspaces';
const { t } = useI18n(); const { t } = useI18n();
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();
const scratchpadStore = useScratchpadStore(); const scratchpadStore = useScratchpadStore();
const workspacesStore = useWorkspacesStore();
const { notes } = storeToRefs(scratchpadStore); const { connectionNotes, selectedTag } = storeToRefs(scratchpadStore);
const { changeNotes } = scratchpadStore; const { changeNotes } = scratchpadStore;
const { hideScratchpad } = applicationStore; const { hideScratchpad } = applicationStore;
const { getConnectionName } = useConnectionsStore();
const { connections } = storeToRefs(useConnectionsStore());
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspaceTab, getWorkspace, newTab, updateTabContent } = workspacesStore;
const localNotes = ref(notes.value); const localConnection = ref(null);
const debounceTimeout: Ref<NodeJS.Timeout> = ref(null); const table: Ref<HTMLDivElement> = ref(null);
const resultTable: Ref<Component & { updateWindow: () => void }> = ref(null);
const tableWrapper: Ref<HTMLDivElement> = ref(null);
const noteFilters: Ref<HTMLInputElement> = ref(null);
const searchForm: Ref<HTMLInputElement> = ref(null);
const resultsSize = ref(1000);
const searchTermInterval: Ref<NodeJS.Timeout> = ref(null);
const scrollElement: Ref<HTMLDivElement> = ref(null);
const searchTerm = ref('');
const localSearchTerm = ref('');
const showArchived = ref(false);
const isAddModal = ref(false);
const isEditModal = ref(false);
const noteToEdit: Ref<ConnectionNote> = ref(null);
const selectedNote = ref(null);
watch(localNotes, () => { const noteTags: ComputedRef<{code: TagCode; name: string}[]> = computed(() => [
clearTimeout(debounceTimeout.value); { code: 'note', name: t('application.note') },
{ code: 'todo', name: 'TODO' },
{ code: 'query', name: 'Query' }
]);
const filteredNotes = computed(() => connectionNotes.value.filter(n => (
(n.type === selectedTag.value || selectedTag.value === 'all') &&
(n.cUid === localConnection.value || localConnection.value === null) &&
(!n.isArchived || showArchived.value) &&
(n.note.toLowerCase().search(localSearchTerm.value.toLowerCase()) >= 0)
)));
const connectionOptions = computed(() => {
return [
{ code: null, name: t('general.all') },
...connections.value.map(c => ({ code: c.uid, name: getConnectionName(c.uid) }))
];
});
debounceTimeout.value = setTimeout(() => { provide('noteTags', noteTags);
changeNotes(localNotes.value); provide('connectionOptions', connectionOptions);
provide('selectedConnection', localConnection);
provide('selectedTag', selectedTag);
const resizeResults = () => {
if (resultTable.value) {
const el = tableWrapper.value.parentElement;
if (el)
resultsSize.value = el.offsetHeight - searchForm.value.offsetHeight - noteFilters.value.offsetHeight;
resultTable.value.updateWindow();
}
};
const refreshScroller = () => resizeResults();
const setTag = (tag: string) => {
selectedTag.value = tag;
};
const toggleNote = (uid: string) => {
selectedNote.value = selectedNote.value !== uid ? uid : null;
};
const startEditNote = (note: ConnectionNote) => {
isEditModal.value = true;
noteToEdit.value = note;
};
const archiveNote = (uid: string) => {
const remappedNotes = connectionNotes.value.map(n => {
if (n.uid === uid)
n.isArchived = true;
return n;
});
changeNotes(remappedNotes);
};
const restoreNote = (uid: string) => {
const remappedNotes = connectionNotes.value.map(n => {
if (n.uid === uid)
n.isArchived = false;
return n;
});
changeNotes(remappedNotes);
};
const deleteNote = (uid: string) => {
const otherNotes = connectionNotes.value.filter(n => n.uid !== uid);
changeNotes(otherNotes);
};
const selectQuery = (query: string) => {
const workspace = getWorkspace(selectedWorkspace.value);
const selectedTab = getWorkspaceTab(workspace.selectedTab);
if (workspace.connectionStatus !== 'connected') return;
if (selectedTab.type === 'query') {
updateTabContent({
tab: selectedTab.uid,
uid: selectedWorkspace.value,
type: 'query',
content: query,
schema: workspace.breadcrumbs.schema
});
}
else {
newTab({
uid: selectedWorkspace.value,
type: 'query',
content: query,
autorun: false,
schema: workspace.breadcrumbs.schema
});
}
hideScratchpad();
};
const closeEditModal = () => {
isEditModal.value = false;
noteToEdit.value = null;
};
watch(searchTerm, () => {
clearTimeout(searchTermInterval.value);
searchTermInterval.value = setTimeout(() => {
localSearchTerm.value = searchTerm.value;
}, 200); }, 200);
}); });
onUpdated(() => {
if (table.value)
refreshScroller();
if (tableWrapper.value)
scrollElement.value = tableWrapper.value;
});
onMounted(() => {
resizeResults();
window.addEventListener('resize', resizeResults);
if (selectedWorkspace.value && selectedWorkspace.value !== 'NEW')
localConnection.value = selectedWorkspace.value;
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeResults);
clearInterval(searchTermInterval.value);
});
</script> </script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.add-button{
border: none;
height: 48px;
width: 48px;
border-radius: 50%;
position: fixed;
margin-top: -40px;
margin-left: 580px;
z-index: 9;
}
.archived-button {
border-radius: 50%;
width: 36px;
height: 36px;
}
</style>

View File

@ -59,17 +59,16 @@
<div class="settingbar-bottom-elements"> <div class="settingbar-bottom-elements">
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<li <li
v-if="!disableScratchpad"
v-tooltip="{ v-tooltip="{
strategy: 'fixed', strategy: 'fixed',
placement: 'right', placement: 'right',
content: t('application.scratchpad') content: t('application.note', 2)
}" }"
class="settingbar-element btn btn-link" class="settingbar-element btn btn-link"
@click="showScratchpad" @click="showScratchpad()"
> >
<BaseIcon <BaseIcon
icon-name="mdiNotebookEditOutline" icon-name="mdiNotebookOutline"
class="settingbar-element-icon text-light" class="settingbar-element-icon text-light"
:size="24" :size="24"
/> />
@ -84,12 +83,15 @@
@click="showSettingModal('general')" @click="showSettingModal('general')"
> >
<div class="settingbar-element-icon-wrapper"> <div class="settingbar-element-icon-wrapper">
<BaseIcon <div
icon-name="mdiCog"
class="settingbar-element-icon text-light" class="settingbar-element-icon text-light"
:class="{ 'badge badge-update': hasUpdates }" :class="{ 'badge badge-update': hasUpdates }"
:size="24" >
/> <BaseIcon
icon-name="mdiCog"
:size="24"
/>
</div>
</div> </div>
</li> </li>
</ul> </ul>
@ -108,7 +110,6 @@ import SettingBarConnections from '@/components/SettingBarConnections.vue';
import SettingBarContext from '@/components/SettingBarContext.vue'; import SettingBarContext from '@/components/SettingBarContext.vue';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { SidebarElement, useConnectionsStore } from '@/stores/connections'; import { SidebarElement, useConnectionsStore } from '@/stores/connections';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
const { t } = useI18n(); const { t } = useI18n();
@ -117,12 +118,10 @@ localStorage.setItem('opened-folders', '[]');
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore(); const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const settingsStore = useSettingsStore();
const { updateStatus } = storeToRefs(applicationStore); const { updateStatus } = storeToRefs(applicationStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { connectionsOrder } = storeToRefs(connectionsStore); const { connectionsOrder } = storeToRefs(connectionsStore);
const { disableScratchpad } = storeToRefs(settingsStore);
const { showSettingModal, showScratchpad } = applicationStore; const { showSettingModal, showScratchpad } = applicationStore;
const { updateConnectionsOrder, initConnectionsOrder } = connectionsStore; const { updateConnectionsOrder, initConnectionsOrder } = connectionsStore;
@ -187,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;
@ -234,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;
@ -266,7 +266,7 @@ if (!connectionsArr.value.length)
.settingbar-element-icon { .settingbar-element-icon {
&.badge::after { &.badge::after {
top: 10px; top: 10px;
right: -6px; right: -3px;
position: absolute; position: absolute;
} }

View File

@ -21,7 +21,7 @@
class="titlebar-element" class="titlebar-element"
@click="openDevTools" @click="openDevTools"
> >
<BaseIcon icon-name="mdiCodeTags" :size="24" /> <BaseIcon icon-name="mdiBugPlayOutline" :size="24" />
</div> </div>
<div <div
v-if="isDevelopment" v-if="isDevelopment"

View File

@ -42,11 +42,11 @@
> >
<BaseIcon <BaseIcon
class="mt-1 mr-1" class="mt-1 mr-1"
icon-name="mdiCodeTags" :icon-name="element.filePath ? 'mdiFileCodeOutline' : 'mdiCodeTags'"
:size="18" :size="18"
/> />
<span> <span>
<span>{{ cutText(element.content || 'Query', 20, true) }} #{{ element.index }}</span> <span>{{ cutText(element.elementName || element.content || 'Query', 20, true) }} #{{ element.index }}</span>
<span <span
class="btn btn-clear" class="btn btn-clear"
:title="t('general.close')" :title="t('general.close')"
@ -63,7 +63,7 @@
> >
<BaseIcon <BaseIcon
class="mt-1 mr-1" class="mt-1 mr-1"
:icon-name="element.elementType === 'view' ? 'mdiTableEye' : 'mdiTable'" :icon-name="['view', 'materializedview'].includes(element.elementType) ? 'mdiTableEye' : 'mdiTable'"
:size="18" :size="18"
/> />
<span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`"> <span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
@ -80,7 +80,7 @@
<a v-else-if="element.type === 'data'" class="tab-link"> <a v-else-if="element.type === 'data'" class="tab-link">
<BaseIcon <BaseIcon
class="mt-1 mr-1" class="mt-1 mr-1"
:icon-name="element.elementType === 'view' ? 'mdiTableEye' : 'mdiTable'" :icon-name="['view', 'materializedview'].includes(element.elementType) ? 'mdiTableEye' : 'mdiTable'"
:size="18" :size="18"
/> />
<span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`"> <span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
@ -157,6 +157,27 @@
</span> </span>
</a> </a>
<a
v-else-if="element.type === 'materialized-view-props'"
class="tab-link"
:class="{'badge': element.isChanged}"
>
<BaseIcon
class="mr-1"
icon-name="mdiWrenchCog"
:size="18"
/>
<span :title="`${t('application.settings').toUpperCase()}: ${t(`database.view`)}`">
{{ cutText(element.elementName, 20, true) }}
<span
class="btn btn-clear"
:title="t('general.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
</span>
</a>
<a <a
v-else-if="element.type === 'new-view'" v-else-if="element.type === 'new-view'"
class="tab-link" class="tab-link"
@ -178,6 +199,27 @@
</span> </span>
</a> </a>
<a
v-else-if="element.type === 'new-materialized-view'"
class="tab-link"
:class="{'badge': element.isChanged}"
>
<BaseIcon
class="mr-1"
icon-name="mdiShapeSquarePlus"
:size="18"
/>
<span :title="`${t('general.new').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
{{ t('database.newMaterializedView') }}
<span
class="btn btn-clear"
:title="t('general.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
</span>
</a>
<a <a
v-else-if="element.type === 'new-trigger'" v-else-if="element.type === 'new-trigger'"
class="tab-link" class="tab-link"
@ -446,6 +488,14 @@
:is-selected="selectedTab === tab.uid && isSelected" :is-selected="selectedTab === tab.uid && isSelected"
:schema="tab.schema" :schema="tab.schema"
/> />
<WorkspaceTabNewMaterializedView
v-else-if="tab.type === 'new-materialized-view'"
:tab-uid="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid && isSelected"
:schema="tab.schema"
/>
<WorkspaceTabPropsView <WorkspaceTabPropsView
v-else-if="tab.type === 'view-props'" v-else-if="tab.type === 'view-props'"
:tab-uid="tab.uid" :tab-uid="tab.uid"
@ -454,6 +504,14 @@
:view="tab.elementName" :view="tab.elementName"
:schema="tab.schema" :schema="tab.schema"
/> />
<WorkspaceTabPropsMaterializedView
v-else-if="tab.type === 'materialized-view-props'"
:tab-uid="tab.uid"
:is-selected="selectedTab === tab.uid && isSelected"
:connection="connection"
:view="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabNewTrigger <WorkspaceTabNewTrigger
v-else-if="tab.type === 'new-trigger'" v-else-if="tab.type === 'new-trigger'"
:tab-uid="tab.uid" :tab-uid="tab.uid"
@ -596,6 +654,9 @@ import Connection from '@/ipc-api/Connection';
import { useConsoleStore } from '@/stores/console'; import { useConsoleStore } from '@/stores/console';
import { useWorkspacesStore, WorkspaceTab } from '@/stores/workspaces'; import { useWorkspacesStore, WorkspaceTab } from '@/stores/workspaces';
import WorkspaceTabNewMaterializedView from './WorkspaceTabNewMaterializedView.vue';
import WorkspaceTabPropsMaterializedView from './WorkspaceTabPropsMaterializedView.vue';
const { t } = useI18n(); const { t } = useI18n();
const { cutText } = useFilters(); const { cutText } = useFilters();
@ -638,7 +699,6 @@ const draggableTabs = computed<WorkspaceTab[]>({
get () { get () {
if (workspace.value.customizations.database) if (workspace.value.customizations.database)
return workspace.value.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.value.database); return workspace.value.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.value.database);
else else
return workspace.value.tabs; return workspace.value.tabs;
}, },
@ -689,13 +749,14 @@ const openAsPermanentTab = (tab: WorkspaceTab) => {
const permanentTabs = { const permanentTabs = {
table: 'data', table: 'data',
view: 'data', view: 'data',
materializedView: 'data',
trigger: 'trigger-props', trigger: 'trigger-props',
triggerFunction: 'trigger-function-props', triggerFunction: 'trigger-function-props',
function: 'function-props', function: 'function-props',
routine: 'routine-props', routine: 'routine-props',
procedure: 'routine-props', procedure: 'routine-props',
scheduler: 'scheduler-props' scheduler: 'scheduler-props'
} as {[key: string]: string}; } as Record<string, string>;
newTab({ newTab({
uid: props.connection.uid, uid: props.connection.uid,

View File

@ -163,22 +163,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="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }} <input v-model="connection.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="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }} <input v-model="connection.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="connection.singleConnectionMode" type="checkbox"><i class="form-icon" /> {{ t('connection.singleConnection') }}
</label>
</div>
</div>
</fieldset> </fieldset>
</form> </form>
</div> </div>
@ -572,11 +580,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

@ -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>
@ -569,11 +577,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));

View File

@ -11,7 +11,7 @@
> >
<div class="workspace-explorebar-header"> <div class="workspace-explorebar-header">
<div <div
v-if="customizations.database" v-if="customizations.database && databases.length"
class="workspace-explorebar-database-switch" class="workspace-explorebar-database-switch"
:title="t('database.switchDatabase')" :title="t('database.switchDatabase')"
> >
@ -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>
@ -34,7 +36,6 @@
</div> </div>
<div :title="t('general.refresh')"> <div :title="t('general.refresh')">
<BaseIcon <BaseIcon
v-if="customizations.schemas"
icon-name="mdiRefresh" icon-name="mdiRefresh"
:size="18" :size="18"
class="c-hand mr-2" class="c-hand mr-2"
@ -110,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')"
@ -140,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"
@ -502,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

@ -55,6 +55,14 @@
/> {{ t('general.disable') }} /> {{ t('general.disable') }}
</span> </span>
</div> </div>
<div class="context-element" @click="copyName(selectedMisc.name)">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copyName') }}</span>
</div>
<div class="context-element" @click="showDeleteModal"> <div class="context-element" @click="showDeleteModal">
<span class="d-flex"> <span class="d-flex">
<BaseIcon <BaseIcon
@ -108,6 +116,7 @@ import Functions from '@/ipc-api/Functions';
import Routines from '@/ipc-api/Routines'; 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 { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@ -163,6 +172,11 @@ const deleteMessage = computed(() => {
} }
}); });
const copyName = (name: string) => {
copyText(name);
closeContext();
};
const showDeleteModal = () => { const showDeleteModal = () => {
isDeleteModal.value = true; isDeleteModal.value = true;
}; };

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
@ -79,6 +177,11 @@
icon-name="mdiFolderCog" icon-name="mdiFolderCog"
:size="18" :size="18"
/> />
<BaseIcon
class="misc-icon open-folder mr-1"
icon-name="mdiFolderOpen"
:size="18"
/>
{{ t('database.trigger', 2) }} {{ t('database.trigger', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
@ -134,6 +237,11 @@
icon-name="mdiFolderSync" icon-name="mdiFolderSync"
:size="18" :size="18"
/> />
<BaseIcon
class="misc-icon open-folder mr-1"
icon-name="mdiFolderOpen"
:size="18"
/>
{{ t('database.storedRoutine', 2) }} {{ t('database.storedRoutine', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
@ -177,6 +285,11 @@
:size="18" :size="18"
:style="`min-width: 18px`" :style="`min-width: 18px`"
/> />
<BaseIcon
class="misc-icon open-folder mr-1"
icon-name="mdiFolderOpen"
:size="18"
/>
{{ t('database.triggerFunction', 2) }} {{ t('database.triggerFunction', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
@ -218,6 +331,11 @@
icon-name="mdiFolderMove" icon-name="mdiFolderMove"
:size="18" :size="18"
/> />
<BaseIcon
class="misc-icon open-folder mr-1"
icon-name="mdiFolderOpen"
:size="18"
/>
{{ t('database.function', 2) }} {{ t('database.function', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
@ -260,6 +378,11 @@
icon-name="mdiFolderClock" icon-name="mdiFolderClock"
:size="18" :size="18"
/> />
<BaseIcon
class="misc-icon open-folder mr-1"
icon-name="mdiFolderOpen"
:size="18"
/>
{{ t('database.scheduler', 2) }} {{ t('database.scheduler', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
@ -355,11 +478,25 @@ 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); return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && 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);
@ -488,7 +625,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 });
}; };
@ -645,10 +788,19 @@ defineExpose({ selectSchema, schemaAccordion });
.database-misc { .database-misc {
margin-left: 1.6rem; margin-left: 1.6rem;
.accordion[open] .accordion-header > .misc-icon:first-child::before { .open-folder {
content: "\F0770"; display: none;
} }
.accordion[open] .accordion-header {
> .misc-icon {
display: none;
&.open-folder {
display: initial;
}
}
}
.accordion-body { .accordion-body {
margin-bottom: 0.2rem; margin-bottom: 0.2rem;
} }

View File

@ -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"
@ -102,6 +114,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="context-element" @click="copyName(selectedSchema)">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copyName') }}</span>
</div>
<div <div
v-if="workspace.customizations.schemaExport" v-if="workspace.customizations.schemaExport"
class="context-element" class="context-element"
@ -198,6 +218,7 @@ import ModalEditSchema from '@/components/ModalEditSchema.vue';
import ModalImportSchema from '@/components/ModalImportSchema.vue'; 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 { 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';
@ -212,6 +233,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',
@ -248,6 +270,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');
}; };
@ -268,6 +294,11 @@ const openCreateSchedulerTab = () => {
emit('open-create-scheduler-tab'); emit('open-create-scheduler-tab');
}; };
const copyName = (name: string) => {
copyText(name);
closeContext();
};
const showDeleteModal = () => { const showDeleteModal = () => {
isDeleteModal.value = true; isDeleteModal.value = true;
}; };

View File

@ -15,6 +15,14 @@
:size="18" :size="18"
/> {{ t('application.settings') }}</span> /> {{ t('application.settings') }}</span>
</div> </div>
<div class="context-element" @click="copyName(selectedTable.name)">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copyName') }}</span>
</div>
<div <div
v-if="selectedTable && selectedTable.type === 'table' && customizations.schemaExport" v-if="selectedTable && selectedTable.type === 'table' && customizations.schemaExport"
class="context-element" class="context-element"
@ -39,6 +47,18 @@
:size="18" :size="18"
/> {{ t('application.settings') }}</span> /> {{ t('application.settings') }}</span>
</div> </div>
<div
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 <div
v-if="selectedTable && selectedTable.type === 'table' && customizations.tableDuplicate" v-if="selectedTable && selectedTable.type === 'table' && customizations.tableDuplicate"
class="context-element" class="context-element"
@ -130,6 +150,7 @@ import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseContextMenu from '@/components/BaseContextMenu.vue'; 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 { 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';
@ -170,6 +191,11 @@ const showTableExportModal = () => {
closeContext(); closeContext();
}; };
const copyName = (name: string) => {
copyText(name);
closeContext();
};
const showDeleteModal = () => { const showDeleteModal = () => {
isDeleteModal.value = true; isDeleteModal.value = true;
}; };
@ -224,6 +250,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

@ -24,7 +24,7 @@
tabindex="0" tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)" @contextmenu.prevent="contextMenu($event, wLog)"
> >
<span class="type-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="query-console-log-sql">{{ wLog.sql }}</code> <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>
</div> </div>
@ -47,11 +47,13 @@
<script setup lang="ts"> <script setup lang="ts">
import * as moment from 'moment'; import * as moment from 'moment';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { highlight } from 'sql-highlight';
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue'; import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import { copyText } from '@/libs/copyText';
import { useConsoleStore } from '@/stores/console'; import { useConsoleStore } from '@/stores/console';
const { t } = useI18n(); const { t } = useI18n();
@ -100,7 +102,7 @@ const contextMenu = (event: MouseEvent, wLog: {date: Date; sql: string}) => {
}; };
const copyQuery = () => { const copyQuery = () => {
navigator.clipboard.writeText(contextQuery.value); copyText(contextQuery.value);
isContext.value = false; isContext.value = false;
}; };
@ -143,7 +145,7 @@ onMounted(() => {
transition: background 0.2s; transition: background 0.2s;
&:hover { &:hover {
background: rgba($primary-color, 50%); background: var(--primary-color-dark);
} }
} }

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