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

Compare commits

...

107 Commits

Author SHA1 Message Date
d20414b692 chore(release): 0.1.0 2021-03-21 13:01:35 +01:00
22a8c25717 fix: update or delete rows with more than one primary key 2021-03-21 13:00:27 +01:00
db47b4040a fix(PostgreSQL): issue getting foreign keys informations 2021-03-21 11:51:22 +01:00
e89911b185 fix: remove last char from datetime and time if is a dot 2021-03-20 16:29:56 +01:00
fccfe92453 fix(PostgreSQL): various issues in query results 2021-03-19 18:49:26 +01:00
d465e18dba feat(PostgreSQL): support to microseconds 2021-03-18 15:56:52 +01:00
ffb1712a59 feat(UI): support to boolean fields 2021-03-18 12:59:46 +01:00
9f6a183d9b fix(PostgreSQL): single quote escape 2021-03-18 12:30:06 +01:00
1f80a64fe1 feat(PostgreSQL): insert and edit blob fields 2021-03-18 11:09:50 +01:00
fc651149b9 feat(PostgreSQL): edit array and text search fields 2021-03-17 18:06:17 +01:00
964570247f feat(PostgreSQL): database in connection parameters 2021-03-17 16:51:26 +01:00
8a6c59f7ce fix: schema content not loaded if selected with right click 2021-03-17 11:57:47 +01:00
4d844fe2c9 refactor: rename database to schema 2021-03-17 11:15:14 +01:00
d892fa6fb3 feat(PostgreSQL): partial postgre implementation 2021-03-16 18:42:03 +01:00
8c9e4f6e96 chore: update issue templates 2021-03-16 15:55:11 +01:00
966ca60c89 chore: update README.md 2021-03-16 15:51:21 +01:00
9bbe218f90 chore: update README.md 2021-03-14 15:38:55 +01:00
a1c6be372b fix(MySQL): handle NEWDECIMAL data type 2021-03-14 15:04:20 +01:00
7d0c929fb8 chore(release): 0.0.20 2021-03-13 19:30:57 +01:00
25d72e3952 fix(UI): row mark not applied on first click 2021-03-10 15:55:34 +01:00
b232a3bb5f feat(UI): loader layers on query and data tabs 2021-03-09 19:14:02 +01:00
e9a26c1bc0 fix(UI): avoid unnecessary updates when cell content not change 2021-03-09 18:08:57 +01:00
76c5c0c680 fix(UI): table rows lose internal id after an update 2021-03-09 18:07:48 +01:00
ddfb713124 feat(UI): row markers in sql editors 2021-03-08 18:11:00 +01:00
0081a4167c refactor: moving from keytar to local storage due issues on Linux 2021-03-08 17:35:43 +01:00
239cb4488f ci: snap store config 2021-03-08 12:40:56 +01:00
fb5adbe676 chore: update package.json to support Windows portable build 2021-03-06 17:29:00 +01:00
9cd51c8d8b chore: update package.json 2021-03-06 17:11:03 +01:00
8dfaa3b7be chore(release): 0.0.19 2021-03-05 17:29:12 +01:00
5d7efa75b7 fix(UI): modal processes list does not regain size on window resize 2021-03-05 17:23:13 +01:00
4862d51fba fix(UI): wrong height in scrolling tables in some cases 2021-03-05 17:10:52 +01:00
07f60c3917 feat(UI): modal that shows process query 2021-03-04 19:34:18 +01:00
049143d143 feat: processes list tool 2021-03-03 19:31:05 +01:00
db4430609e feat(MySQL): support to new mysql8 authentication, closes #45 2021-03-02 12:03:01 +01:00
71b4310117 feat: context menu shortcut to set NULL a table cell 2021-02-28 21:45:38 +01:00
201fad9265 fix(MySQL): wrong TIMESTAMP fields length 2021-02-27 18:30:34 +01:00
45351faeae feat(UI): esc key to cancel cell edit 2021-02-27 18:29:47 +01:00
b4ead6992c perf(UI): improvements of date time inputs 2021-02-27 17:52:54 +01:00
b1ea32b680 feat: setting to enable beta updates (future use) 2021-02-27 17:28:01 +01:00
39ca1974bc perf(UI): big performance improvement in tables rendering 2021-02-26 22:31:05 +01:00
777b73fa6f feat(UI): query duration calc 2021-02-26 18:45:00 +01:00
4494e637f7 chore(release): 0.0.18 2021-02-25 19:09:02 +01:00
219da0aba4 feat(UI): run procedures/functions from sidebar context menu 2021-02-25 17:43:23 +01:00
7e8167154f feat(UI): run routines/functions from settings tab 2021-02-25 12:39:50 +01:00
3aa2159a1a fix(MySQL): issue obtaining routine/function parameters 2021-02-24 19:45:27 +01:00
76d92cd106 fix: issue managing function/routine parameters 2021-02-24 12:46:31 +01:00
c8545a250b fix(UI): elements from previous selected schemas in query suggestions 2021-02-22 19:14:02 +01:00
dbab06fcb8 fix(UI): data tab opened when non-table element is selected 2021-02-22 11:10:04 +01:00
b54fefbf25 feat(UI): context menu for input and textarea tags 2021-02-21 21:24:25 +01:00
9a1bf32128 feat(UI): html, xml, json, svg and yaml editor modes in long text fields edit 2021-02-21 19:22:03 +01:00
110b0b414c feat(UI): sticky schema names in explore bar 2021-02-20 18:55:08 +01:00
2f58007af4 feat(UI): search filter in explore bar 2021-02-20 11:55:34 +01:00
9b60bfff8d build: dropped use of lodash 2021-02-19 17:41:33 +01:00
3b37b7432e fix: disabled sort for fields without a name property 2021-02-18 18:12:36 +01:00
7c4ca999ce fix: prevents F5 shortcut to run in non-selected workspaces 2021-02-18 18:01:12 +01:00
94c4952319 fix: support of bit fields in table filler 2021-02-18 15:26:17 +01:00
014257147e chore(release): 0.0.17 2021-02-17 18:50:45 +01:00
970de4962b feat: support to fake data locales 2021-02-17 18:49:02 +01:00
b5a828309f fix(UI): file uploader in table filler 2021-02-17 14:47:15 +01:00
5b21d17f3a fix(UI): no foreign key select editing query results 2021-02-17 14:17:50 +01:00
2c6e35288f chore: update README.md 2021-02-17 09:13:50 +01:00
bcadac6e95 Merge pull request #44 from MrAnyx/master
feat: added french language
2021-02-17 09:04:38 +01:00
MrAnyx
18a93ef1aa Feat: Added french language 2021-02-16 20:35:19 +01:00
6c62052b47 feat: min and max option for random floats and numbers 2021-02-16 19:13:20 +01:00
9d5ebefdce fix: wrong date or time detection in field default 2021-02-15 09:58:43 +01:00
34ebc6b72d chore: update README.md 2021-02-15 09:11:48 +01:00
288ff4c1a1 fix: cut faker text based on field length 2021-02-14 18:25:57 +01:00
a176174b8d feat: fake table data generator 2021-02-13 18:45:16 +01:00
0f69d1dbb7 fix(UI): wrong length for char fields on table header 2021-02-12 18:02:18 +01:00
0386bbac50 refactor: number and float fields as separate types 2021-02-10 18:24:28 +01:00
b0576acdf6 perf(core): bulk inserts support 2021-02-08 11:46:57 +01:00
9a190854fe fix(UI): better text on ssl file selectors 2021-02-08 09:36:44 +01:00
2aace28e80 chore(release): 0.0.16 2021-02-06 12:41:42 +01:00
02c03e3d26 feat: MySQL and MariaDB auto detection 2021-02-06 12:37:37 +01:00
a0d85520fb feat(UI): enanched file upload input 2021-02-05 19:37:35 +01:00
ede6fe81ce fix: edit bit fields 2021-02-04 09:20:52 +01:00
4e72bb1587 feat: support to ssl connections 2021-02-03 21:53:24 +01:00
15417e8a77 feat(UI): database version in app footer 2021-02-01 16:31:48 +01:00
88ab7c5a62 feat(UI): resize query editor area 2021-01-31 13:03:25 +01:00
5940b0b842 feat: edit rows from tables without a primary key 2021-01-30 14:58:12 +01:00
574d493908 feat: delete rows from tables without a primary key 2021-01-28 18:33:29 +01:00
af96647603 refactor: minor UI improvements 2021-01-25 18:28:22 +01:00
ab70ff239e build: update dependencies 2021-01-25 09:29:36 +01:00
bacf458936 fix: compatibility with electron-store 7 2021-01-25 09:28:57 +01:00
379d6c169a Merge pull request #43 from Fabio286/dependabot/npm_and_yarn/electron-store-7.0.0
build(deps): bump electron-store from 6.0.1 to 7.0.0
2021-01-25 08:04:09 +01:00
dependabot[bot]
36352f8028 build(deps): bump electron-store from 6.0.1 to 7.0.0
Bumps [electron-store](https://github.com/sindresorhus/electron-store) from 6.0.1 to 7.0.0.
- [Release notes](https://github.com/sindresorhus/electron-store/releases)
- [Commits](https://github.com/sindresorhus/electron-store/compare/v6.0.1...v7.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-25 06:46:59 +00:00
04fcaf2f6e chore(release): 0.0.15 2021-01-23 16:06:55 +01:00
8b914446b0 chore: standard-version configuration for perf commits 2021-01-23 16:06:21 +01:00
a11bac504c perf: big performance improvement in database structure loading 2021-01-23 15:50:21 +01:00
b9ed8dd610 fix: error retriving dato of some schedulers 2021-01-22 18:46:33 +01:00
1cf6485896 feat: loading animation in properties tabs 2021-01-22 18:27:45 +01:00
4bc9bbfb34 perf: better fields type detection 2021-01-21 18:14:37 +01:00
4923128236 fix: unable to call stored routines from query tabs 2021-01-19 19:14:11 +01:00
8ff6e70145 feat: functions and schedulers in query suggestions 2021-01-18 18:41:28 +01:00
afcf1c86ed chore(release): 0.0.14 2021-01-16 11:47:57 +01:00
0200ae4a0f chore: update README.md 2021-01-16 11:45:44 +01:00
dbe7b9dd23 feat: schedulers creation 2021-01-16 11:32:42 +01:00
ceab4ef243 feat: scheduler edit 2021-01-15 19:18:16 +01:00
1e7d4ca347 feat: schedulers delete 2021-01-14 18:11:36 +01:00
c0a32c040e fix: removed internal row _id from exported files 2021-01-13 11:57:26 +01:00
f150508547 fix: error with empty functions/procedures 2021-01-11 18:56:51 +01:00
49d71722e2 feat: functions creation 2021-01-11 09:55:13 +01:00
59a50bc014 feat: functions delete 2021-01-10 18:40:19 +01:00
41d75b127c feat: functions edit 2021-01-10 18:30:56 +01:00
0cbea9d100 feat: export data tables to json or csv file 2021-01-08 21:55:03 +01:00
e351c903a8 feat: triggers and stored routines in sql suggestions 2021-01-07 18:22:49 +01:00
6e55f27b23 chore: update README.md 2021-01-06 12:02:41 +01:00
98 changed files with 9368 additions and 1095 deletions

View File

@@ -3,7 +3,7 @@ name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
assignees: Fabio286
---
@@ -25,13 +25,6 @@ If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**

View File

@@ -3,7 +3,7 @@ name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
assignees: Fabio286
---

7
.versionrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"types": [
{"type":"feat","section":"Features"},
{"type":"perf","section":"Improvements"},
{"type":"fix","section":"Bug Fixes"}
]
}

View File

@@ -2,6 +2,177 @@
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.1.0](https://github.com/Fabio286/antares/compare/v0.0.20...v0.1.0) (2021-03-21)
### Features
* **PostgreSQL:** database in connection parameters ([9645702](https://github.com/Fabio286/antares/commit/964570247ff5b7b8317419730eec5bed4f0f0580))
* **PostgreSQL:** edit array and text search fields ([fc65114](https://github.com/Fabio286/antares/commit/fc651149b95399c52d2d63e946731e9c1b0303a9))
* **PostgreSQL:** insert and edit blob fields ([1f80a64](https://github.com/Fabio286/antares/commit/1f80a64fe1400baacca26f1a762c5aeb4ef6350d))
* **PostgreSQL:** partial postgre implementation ([d892fa6](https://github.com/Fabio286/antares/commit/d892fa6fb3e86fbb96887d8eb67319ae855260a1))
* **PostgreSQL:** support to microseconds ([d465e18](https://github.com/Fabio286/antares/commit/d465e18dba8ea3aa00726e33f9b1f70ca4c0683c))
* **UI:** support to boolean fields ([ffb1712](https://github.com/Fabio286/antares/commit/ffb1712a593d1421793011e48a17369b884ea3c0))
### Bug Fixes
* update or delete rows with more than one primary key ([22a8c25](https://github.com/Fabio286/antares/commit/22a8c25717a4d4b285855426098a3a2846ce7448))
* **MySQL:** handle NEWDECIMAL data type ([a1c6be3](https://github.com/Fabio286/antares/commit/a1c6be372b570cf13e89ef7ecf9b7a7c033a9293))
* **PostgreSQL:** issue getting foreign keys informations ([db47b40](https://github.com/Fabio286/antares/commit/db47b4040a5282a6ac0711b1926c4c2ac867999e))
* remove last char from datetime and time if is a dot ([e89911b](https://github.com/Fabio286/antares/commit/e89911b1851c19813d4acf2c79adfbc2ac7c1112))
* **PostgreSQL:** single quote escape ([9f6a183](https://github.com/Fabio286/antares/commit/9f6a183d9b293dfe9ad9f3759f2375f05f37db8e))
* **PostgreSQL:** various issues in query results ([fccfe92](https://github.com/Fabio286/antares/commit/fccfe92453325cd54c0331cc5670af0a56822c5b))
* schema content not loaded if selected with right click ([8a6c59f](https://github.com/Fabio286/antares/commit/8a6c59f7ce7d051315b04cea38a96e4739b5b9d3))
### [0.0.20](https://github.com/Fabio286/antares/compare/v0.0.19...v0.0.20) (2021-03-13)
### Features
* **UI:** loader layers on query and data tabs ([b232a3b](https://github.com/Fabio286/antares/commit/b232a3bb5ff7e38c83aa33e8b96ec7202bc4881e))
* **UI:** row markers in sql editors ([ddfb713](https://github.com/Fabio286/antares/commit/ddfb7131248a47fa2055ccfb72e223986a17f986))
### Bug Fixes
* **UI:** avoid unnecessary updates when cell content not change ([e9a26c1](https://github.com/Fabio286/antares/commit/e9a26c1bc0641b7087d8143cc948405850d7552f))
* **UI:** row mark not applied on first click ([25d72e3](https://github.com/Fabio286/antares/commit/25d72e39529884c09cf1286ff64ba00c8a5c7b24))
* **UI:** table rows lose internal id after an update ([76c5c0c](https://github.com/Fabio286/antares/commit/76c5c0c680521b4a20de1f12bab25314ac084d5c))
### [0.0.19](https://github.com/Fabio286/antares/compare/v0.0.18...v0.0.19) (2021-03-05)
### Features
* **UI:** modal that shows process query ([07f60c3](https://github.com/Fabio286/antares/commit/07f60c39173e9db452909d74573c3aecf4b6466b))
* processes list tool ([049143d](https://github.com/Fabio286/antares/commit/049143d143d8187bfcb6377f2bf374c471c28046))
* **MySQL:** support to new mysql8 authentication, closes [#45](https://github.com/Fabio286/antares/issues/45) ([db44306](https://github.com/Fabio286/antares/commit/db4430609e22816f4a3a8ecdbf61e9b51cde2579))
* context menu shortcut to set NULL a table cell ([71b4310](https://github.com/Fabio286/antares/commit/71b43101172c27d810d533c936534bcf089dbca2))
* **UI:** esc key to cancel cell edit ([45351fa](https://github.com/Fabio286/antares/commit/45351faeaea6ad4af8280da6c0ec1b4ded0f86fe))
* setting to enable beta updates (future use) ([b1ea32b](https://github.com/Fabio286/antares/commit/b1ea32b68024593481120e2ef67642e127554888))
* **UI:** query duration calc ([777b73f](https://github.com/Fabio286/antares/commit/777b73fa6f9d6f7806e0d3d07589dfaee0c40786))
### Bug Fixes
* **MySQL:** wrong TIMESTAMP fields length ([201fad9](https://github.com/Fabio286/antares/commit/201fad9265e01e19ae4c8dc16bff84ee9dc9c894))
* **UI:** modal processes list does not regain size on window resize ([5d7efa7](https://github.com/Fabio286/antares/commit/5d7efa75b76f59b85654603b4df8035a36af0576))
* **UI:** wrong height in scrolling tables in some cases ([4862d51](https://github.com/Fabio286/antares/commit/4862d51fba863e055ca6735586b0abe4894037eb))
### Improvements
* **UI:** big performance improvement in tables rendering ([39ca197](https://github.com/Fabio286/antares/commit/39ca1974bcea84c9047779893ed3f458300474e3))
* **UI:** improvements of date time inputs ([b4ead69](https://github.com/Fabio286/antares/commit/b4ead6992c8ddc172380a97e7aa125e0dd078f1e))
### [0.0.18](https://github.com/Fabio286/antares/compare/v0.0.17...v0.0.18) (2021-02-25)
### Features
* **UI:** context menu for input and textarea tags ([b54fefb](https://github.com/Fabio286/antares/commit/b54fefbf255c91f3cdd0c52b917b239ea40dae16))
* **UI:** html, xml, json, svg and yaml editor modes in long text fields edit ([9a1bf32](https://github.com/Fabio286/antares/commit/9a1bf3212850d3783ac6382f8a980eb25c4f4226))
* **UI:** run procedures/functions from sidebar context menu ([219da0a](https://github.com/Fabio286/antares/commit/219da0aba46839295807a01056199d42584e0af9))
* **UI:** run routines/functions from settings tab ([7e81671](https://github.com/Fabio286/antares/commit/7e8167154ffd64e46d78a50cd13cf90cee61370c))
* **UI:** search filter in explore bar ([2f58007](https://github.com/Fabio286/antares/commit/2f58007af4f32207fe4be9fc15bb43916e8e0abc))
* **UI:** sticky schema names in explore bar ([110b0b4](https://github.com/Fabio286/antares/commit/110b0b414cef25395ae28493032c2048307e4783))
### Bug Fixes
* **MySQL:** issue obtaining routine/function parameters ([3aa2159](https://github.com/Fabio286/antares/commit/3aa2159a1ae3b6785646fc7ac1e866cb0277d087))
* issue managing function/routine parameters ([76d92cd](https://github.com/Fabio286/antares/commit/76d92cd106b0409e8752c43d7d587d09dd3e1e32))
* **UI:** data tab opened when non-table element is selected ([dbab06f](https://github.com/Fabio286/antares/commit/dbab06fcb80ccdae85215ceae9ba24af3e7ff263))
* **UI:** elements from previous selected schemas in query suggestions ([c8545a2](https://github.com/Fabio286/antares/commit/c8545a250bc696c339e418a69a2942d5bac10cbf))
* disabled sort for fields without a name property ([3b37b74](https://github.com/Fabio286/antares/commit/3b37b7432e969e6315ebd6ed74905ec3980a049c))
* prevents F5 shortcut to run in non-selected workspaces ([7c4ca99](https://github.com/Fabio286/antares/commit/7c4ca999ce64af8077d29e419f430d5beaaead93))
* support of bit fields in table filler ([94c4952](https://github.com/Fabio286/antares/commit/94c49523197284510361ebbb6984816a3bb1b243))
### [0.0.17](https://github.com/Fabio286/antares/compare/v0.0.16...v0.0.17) (2021-02-17)
### Features
* Added french language ([18a93ef](https://github.com/Fabio286/antares/commit/18a93ef1aae530e69c9062bbeb08a3beec206eda))
* fake table data generator ([a176174](https://github.com/Fabio286/antares/commit/a176174b8d7dc232920f4cd7c5e3f8e4c58d51a0))
* min and max option for random floats and numbers ([6c62052](https://github.com/Fabio286/antares/commit/6c62052b4764731c774fef90784342447b36deb7))
* support to fake data locales ([970de49](https://github.com/Fabio286/antares/commit/970de4962b3bffec271bfa6d4747e6fb9d408ba6))
### Bug Fixes
* **UI:** file uploader in table filler ([b5a8283](https://github.com/Fabio286/antares/commit/b5a828309f067636eae2120032f07e01233706e2))
* **UI:** no foreign key select editing query results ([5b21d17](https://github.com/Fabio286/antares/commit/5b21d17f3a8f2482c3aafe85f26c34cd3c0a1fcf))
* cut faker text based on field length ([288ff4c](https://github.com/Fabio286/antares/commit/288ff4c1a1c77f4b8b86b24649d805836366fdd3))
* wrong date or time detection in field default ([9d5ebef](https://github.com/Fabio286/antares/commit/9d5ebefdced999af595f3d8dc2fac2b18fa8258b))
* **UI:** better text on ssl file selectors ([9a19085](https://github.com/Fabio286/antares/commit/9a190854fe3c73ed4e7f89545ff259e15ee9f947))
* **UI:** wrong length for char fields on table header ([0f69d1d](https://github.com/Fabio286/antares/commit/0f69d1dbb7958e45059b6b738c845abea1ad3225))
### Improvements
* **core:** bulk inserts support ([b0576ac](https://github.com/Fabio286/antares/commit/b0576acdf65d41c1c8e0b0bca6cf6522dcb372be))
### [0.0.16](https://github.com/Fabio286/antares/compare/v0.0.15...v0.0.16) (2021-02-06)
### Features
* MySQL and MariaDB auto detection ([02c03e3](https://github.com/Fabio286/antares/commit/02c03e3d266052872ac8edeb159ec8182f41c6a5))
* **UI:** enanched file upload input ([a0d8552](https://github.com/Fabio286/antares/commit/a0d85520fb0669d5eec4475f5e255adb1e2a0159))
* support to ssl connections ([4e72bb1](https://github.com/Fabio286/antares/commit/4e72bb15874324214aa0f5b89057cdd565744468))
* **UI:** database version in app footer ([15417e8](https://github.com/Fabio286/antares/commit/15417e8a776c6aa9f90762d198d70b26163bb2df))
* **UI:** resize query editor area ([88ab7c5](https://github.com/Fabio286/antares/commit/88ab7c5a62654c6a6026b63464224226d33b1950))
* delete rows from tables without a primary key ([574d493](https://github.com/Fabio286/antares/commit/574d4939083577ffcb8e7c65f572c364eb8415fb))
* edit rows from tables without a primary key ([5940b0b](https://github.com/Fabio286/antares/commit/5940b0b84207093da141b5c617c41e0a18fcb1d7))
### Bug Fixes
* compatibility with electron-store 7 ([bacf458](https://github.com/Fabio286/antares/commit/bacf45893676cb0744907703f6534ffd472bd1dd))
* edit bit fields ([ede6fe8](https://github.com/Fabio286/antares/commit/ede6fe81cefc91cdce2bbb0cd7cd6f85bbca99b8))
### [0.0.15](https://github.com/Fabio286/antares/compare/v0.0.14...v0.0.15) (2021-01-23)
### Features
* functions and schedulers in query suggestions ([8ff6e70](https://github.com/Fabio286/antares/commit/8ff6e70145ed2a207ae8b23a2c688258382a5d74))
* loading animation in properties tabs ([1cf6485](https://github.com/Fabio286/antares/commit/1cf64858964f4894913db42f7c268013bb06e40b))
### Bug Fixes
* error retriving dato of some schedulers ([b9ed8dd](https://github.com/Fabio286/antares/commit/b9ed8dd610e3be1489e01cf53f7d632cb1bd6ac5))
* unable to call stored routines from query tabs ([4923128](https://github.com/Fabio286/antares/commit/4923128236131482ca948ae8052c294bd9269ed0))
### Improvements
* better fields type detection ([4bc9bbf](https://github.com/Fabio286/antares/commit/4bc9bbfb34ebdc51061f718cdf9cbca8507fa0f4))
* big performance improvement in database structure loading ([a11bac5](https://github.com/Fabio286/antares/commit/a11bac504cd4ee865ea6c614a15ee809dc38202e))
### [0.0.14](https://github.com/Fabio286/antares/compare/v0.0.13...v0.0.14) (2021-01-16)
### Features
* export data tables to json or csv file ([0cbea9d](https://github.com/Fabio286/antares/commit/0cbea9d1007304a5b9cf893d165b4b4104266651))
* functions creation ([49d7172](https://github.com/Fabio286/antares/commit/49d71722e26172232f7b54c6568e1e588ce0d049))
* functions delete ([59a50bc](https://github.com/Fabio286/antares/commit/59a50bc014facc9643f9153cff61dc9d5a8605a9))
* functions edit ([41d75b1](https://github.com/Fabio286/antares/commit/41d75b127cbcf1481fd259a14e6e7688638e18a4))
* scheduler edit ([ceab4ef](https://github.com/Fabio286/antares/commit/ceab4ef243881ba64517fb95320844a21fce4849))
* schedulers creation ([dbe7b9d](https://github.com/Fabio286/antares/commit/dbe7b9dd239248e806377ae6236b477456f175a3))
* schedulers delete ([1e7d4ca](https://github.com/Fabio286/antares/commit/1e7d4ca347f4b9337ff266ec78bb4bbc6dd20d4d))
* triggers and stored routines in sql suggestions ([e351c90](https://github.com/Fabio286/antares/commit/e351c903a8a8d7e908d6a7d54c0491438ac6f024))
### Bug Fixes
* error with empty functions/procedures ([f150508](https://github.com/Fabio286/antares/commit/f1505085477a760a768a7d245c9517a858c1379c))
* removed internal row _id from exported files ([c0a32c0](https://github.com/Fabio286/antares/commit/c0a32c040e653729ef80d580d6dd1796d1b2adcd))
### [0.0.13](https://github.com/Fabio286/antares/compare/v0.0.12...v0.0.13) (2021-01-06)

View File

@@ -1,10 +1,10 @@
<p align="center">
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/screen-alpha.png">
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/gh-logo.png">
</p>
# Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares)
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares)
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.
My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
@@ -32,6 +32,8 @@ An application created with minimalism and semplicity in mind, with features in
- Multiple database connections at same time.
- Database management (add/edit/delete).
- Full tables management, including indexes and foreign keys.
- Views, triggers, stored routines, functions and schedulers management (add/edit/delete).
- Fake table data filler.
- Run queries on multiple tabs.
- Query suggestions and auto complete.
- Native dark theme.
@@ -43,19 +45,28 @@ An application created with minimalism and semplicity in mind, with features in
This is a roadmap with major features will come in near future.
- Stored procedures, views, schedulers and triggers support.
- Users management (add/edit/delete).
- Database tools (variables, process list...).
- SSL and SSH tunnel support.
- Support for other databases.
- Database tools (variables, process list...).
- SSH tunnel support.
- Users management (add/edit/delete).
- UI/UX improvements.
- Query history.
- More context menu shortcuts.
- More keyboard shortcuts.
- Query logs console.
- Fake data filler.
- Import/export and migration.
- Themes.
- Light theme.
## Troubleshooting
### **Linux**
With KDE may need necessary installation of the additional `gnome-keyring` package.
Depending on your distribution, you will need to run the following command:
- Debian/Ubuntu: `sudo apt-get install gnome-keyring`
- Red Hat-based: `sudo yum install gnome-keyring`
- Arch Linux: `sudo pacman -S gnome-keyring`
## Currently supported
@@ -63,8 +74,8 @@ This is a roadmap with major features will come in near future.
- [x] MySQL/MariaDB
- [ ] PostgreSQL
- [ ] MSSQL
- [ ] SQLite
- [ ] MSSQL
- [ ] OracleDB
- [ ] More...
@@ -74,7 +85,7 @@ This is a roadmap with major features will come in near future.
- [x] Windows
- [x] Linux
- [x] MacOS (needs tests)
- [x] MacOS (not tested due lack of hardware)
#### • ARM
@@ -84,6 +95,11 @@ This is a roadmap with major features will come in near future.
## Translations
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/Fabio286/antares/pull/20)
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/Fabio286/antares/pull/29)
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/Fabio286/antares/pull/32)
**Italian Translation** (46%) / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
**Arabic Translation** (45%) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
**Spanish Translation** (46%) / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
**French Translation** (100%) / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
## Reviews
<a target="_blank" href="https://www.softx64.com/windows/antares-sql-client.html" title="Antares SQL Client review"><img src="https://www.softx64.com/softx64-review.png" alt="Antares SQL Client review" /></a>

BIN
docs/gh-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.0.13",
"version": "0.1.0",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
@@ -11,6 +11,7 @@
"build": "cross-env NODE_ENV=production npm run compile && electron-builder",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"release:snap": "npm run build -- --linux snap && snapcraft upload --release=edge ./dist/Antares-${npm_package_version}-linux_amd64.snap",
"test": "npm run lint",
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix"
@@ -19,6 +20,12 @@
"build": {
"appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"win": {
"target": [
"nsis",
"portable"
]
},
"dmg": {
"contents": [
{
@@ -39,6 +46,13 @@
"AppImage"
],
"category": "Development"
},
"appImage": {
"license": "./LICENSE",
"category": "Development"
},
"portable": {
"artifactName": "${productName}-${version}-portable.exe"
}
},
"electronWebpack": {
@@ -47,47 +61,48 @@
}
},
"dependencies": {
"@mdi/font": "^5.8.55",
"@mdi/font": "^5.9.55",
"ace-builds": "^1.4.12",
"electron-log": "^4.3.0",
"electron-store": "^6.0.1",
"electron-store": "^7.0.0",
"electron-updater": "^4.3.5",
"faker": "^5.3.1",
"keytar": "^7.3.0",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"mssql": "^6.2.3",
"mysql": "^2.18.1",
"mysql2": "^2.2.5",
"node-sql-parser": "^3.1.0",
"pg": "^8.5.1",
"pgsql-ast-parser": "^7.0.2",
"source-map-support": "^0.5.16",
"spectre.css": "^0.5.9",
"vue-i18n": "^8.22.2",
"vue-the-mask": "^0.11.1",
"v-mask": "^2.2.4",
"vue-i18n": "^8.22.4",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0",
"vuex-persist": "^3.1.3"
"vuex": "^3.6.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.2",
"electron": "^11.0.2",
"electron": "^11.3.0",
"electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.14.0",
"eslint": "^7.20.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-vue": "^7.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-vue": "^7.6.0",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.0",
"standard-version": "^9.0.0",
"stylelint": "^13.8.0",
"sass-loader": "^10.1.1",
"standard-version": "^9.1.0",
"stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"stylelint-scss": "^3.19.0",
"vue": "^2.6.12",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.2"
"webpack": "^4.46.0"
}
}

96
snap/snapcraft.yaml Normal file
View File

@@ -0,0 +1,96 @@
name: antares
adopt-info: antares
summary: Open source SQL client made to be simple and complete.
description: |
Antares is an SQL client that aims to become an useful and complete tool, especially for developers.
The target is to support as many databases as possible, and all major operating systems, including the ARM versions.
At the moment this application is an alpha and supports only MySQL and x86 architecture.
Most of its current features might be enough for MySQL management, so give it a chance and send us your feedback, we would really appreciate it.
base: core18
grade: stable
confinement: strict
architectures:
- build-on: amd64
compression: lzo
parts:
antares:
plugin: dump
source: .
override-build: |
snapcraftctl build
ARCHITECTURE=$(dpkg --print-architecture)
if [ "${ARCHITECTURE}" = "amd64" ]; then
FILTER="amd64.deb"
else
echo "ERROR! Antares only produces debs for amd64. Failing the build here."
exit 1
fi
# Get the latest releases json
echo "Get GitHub releases..."
wget --quiet https://api.github.com/repos/fabio286/antares/releases/latest -O releases.json
# Get the version from the tag_name and the download URL.
VERSION=$(jq . releases.json | grep tag_name | cut -d'"' -f4 | sed s'/release-//')
DEB_URL=$(cat releases.json | jq -r ".assets[] | select(.name | test(\"${FILTER}\")) | .browser_download_url")
DEB=$(basename "${DEB_URL}")
echo "Downloading ${DEB_URL}..."
wget --quiet "${DEB_URL}" -O "${SNAPCRAFT_PART_INSTALL}/${DEB}"
echo "Unpacking ${DEB}..."
dpkg -x "${SNAPCRAFT_PART_INSTALL}/${DEB}" ${SNAPCRAFT_PART_INSTALL}
rm -f releases.json 2>/dev/null
rm -f "${SNAPCRAFT_PART_INSTALL}/${DEB}" 2>/dev/null
echo $VERSION > $SNAPCRAFT_STAGE/version
# Correct path to icon.
sed -i 's|Icon=antares|Icon=/usr/share/icons/hicolor/256x256/apps/antares\.png|g' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/antares.desktop
# Delete usr/bin/antares, it's a broken symlink pointing outside the snap.
rm -f ${SNAPCRAFT_PART_INSTALL}/usr/bin/antares
chmod -s ${SNAPCRAFT_PART_INSTALL}/opt/Antares/chrome-sandbox
snapcraftctl set-version "$(echo $VERSION)"
build-packages:
- dpkg
- jq
- sed
- wget
stage-packages:
- fcitx-frontend-gtk3
- libappindicator3-1
- libasound2
- libgconf-2-4
- libgtk-3-0
- libnotify4
- libnspr4
- libnss3
- libpcre3
- libpulse0
- libxss1
- libsecret-1-0
- libxtst6
- libxkbfile1
cleanup:
after: [antares]
plugin: nil
build-snaps: [ gnome-3-28-1804 ]
override-prime: |
set -eux
cd /snap/gnome-3-28-1804/current
find . -type f,l -exec rm -f $SNAPCRAFT_PRIME/{} \;
apps:
antares:
command: opt/Antares/antares --no-sandbox
desktop: usr/share/applications/antares.desktop
extensions: [gnome-3-28]
environment:
# Fallback to XWayland if running in a Wayland session.
DISABLE_WAYLAND: 1
plugs:
- browser-support
- cups-control
- home
- network
- opengl
- pulseaudio
- removable-media
- unity7

217
src/common/FakerMethods.js Normal file
View File

@@ -0,0 +1,217 @@
export default class {
static get _methods () {
return [
{ name: 'zipCode', group: 'address', types: ['string'] },
{ name: 'zipCodeByState', group: 'address', types: ['string'] },
{ name: 'city', group: 'address', types: ['string'] },
{ name: 'cityPrefix', group: 'address', types: ['string'] },
{ name: 'citySuffix', group: 'address', types: ['string'] },
{ name: 'streetName', group: 'address', types: ['string'] },
{ name: 'streetAddress', group: 'address', types: ['string'] },
{ name: 'streetSuffix', group: 'address', types: ['string'] },
{ name: 'streetPrefix', group: 'address', types: ['string'] },
{ name: 'secondaryAddress', group: 'address', types: ['string'] },
{ name: 'county', group: 'address', types: ['string'] },
{ name: 'country', group: 'address', types: ['string'] },
{ name: 'countryCode', group: 'address', types: ['string'] },
{ name: 'state', group: 'address', types: ['string'] },
{ name: 'stateAbbr', group: 'address', types: ['string'] },
{ name: 'latitude', group: 'address', types: ['string'] },
{ name: 'longitude', group: 'address', types: ['string'] },
{ name: 'direction', group: 'address', types: ['string'] },
{ name: 'cardinalDirection', group: 'address', types: ['string'] },
{ name: 'ordinalDirection', group: 'address', types: ['string'] },
// { name: 'nearbyGPSCoordinate', group: 'address', types: ['string'] },
{ name: 'timeZone', group: 'address', types: ['string'] },
{ name: 'color', group: 'commerce', types: ['string'] },
{ name: 'department', group: 'commerce', types: ['string'] },
{ name: 'productName', group: 'commerce', types: ['string'] },
{ name: 'price', group: 'commerce', types: ['string', 'float'] },
{ name: 'productAdjective', group: 'commerce', types: ['string'] },
{ name: 'productMaterial', group: 'commerce', types: ['string'] },
{ name: 'product', group: 'commerce', types: ['string'] },
{ name: 'productDescription', group: 'commerce', types: ['string'] },
{ name: 'suffixes', group: 'company', types: ['string'] },
{ name: 'companyName', group: 'company', types: ['string'] },
{ name: 'companySuffix', group: 'company', types: ['string'] },
{ name: 'catchPhrase', group: 'company', types: ['string'] },
{ name: 'bs', group: 'company', types: ['string'] },
{ name: 'catchPhraseAdjective', group: 'company', types: ['string'] },
{ name: 'catchPhraseDescriptor', group: 'company', types: ['string'] },
{ name: 'catchPhraseNoun', group: 'company', types: ['string'] },
{ name: 'bsAdjective', group: 'company', types: ['string'] },
{ name: 'bsBuzz', group: 'company', types: ['string'] },
{ name: 'bsNoun', group: 'company', types: ['string'] },
{ name: 'column', group: 'database', types: ['string'] },
{ name: 'type', group: 'database', types: ['string'] },
{ name: 'collation', group: 'database', types: ['string'] },
{ name: 'engine', group: 'database', types: ['string'] },
{ name: 'past', group: 'date', types: ['string', 'datetime'] },
{ name: 'future', group: 'date', types: ['string', 'datetime'] },
// { name: 'between', group: 'date', types: ['string'] },
{ name: 'recent', group: 'date', types: ['string', 'datetime'] },
{ name: 'soon', group: 'date', types: ['string', 'datetime'] },
{ name: 'month', group: 'date', types: ['string'] },
{ name: 'weekday', group: 'date', types: ['string'] },
{ name: 'account', group: 'finance', types: ['string', 'number'] },
{ name: 'accountName', group: 'finance', types: ['string'] },
{ name: 'routingNumber', group: 'finance', types: ['string', 'number'] },
{ name: 'mask', group: 'finance', types: ['string', 'number'] },
{ name: 'amount', group: 'finance', types: ['string', 'float'] },
{ name: 'transactionType', group: 'finance', types: ['string'] },
{ name: 'currencyCode', group: 'finance', types: ['string'] },
{ name: 'currencyName', group: 'finance', types: ['string'] },
{ name: 'currencySymbol', group: 'finance', types: ['string'] },
{ name: 'bitcoinAddress', group: 'finance', types: ['string'] },
{ name: 'litecoinAddress', group: 'finance', types: ['string'] },
{ name: 'creditCardNumber', group: 'finance', types: ['string'] },
{ name: 'creditCardCVV', group: 'finance', types: ['string', 'number'] },
{ name: 'ethereumAddress', group: 'finance', types: ['string'] },
{ name: 'iban', group: 'finance', types: ['string'] },
{ name: 'bic', group: 'finance', types: ['string'] },
{ name: 'transactionDescription', group: 'finance', types: ['string'] },
{ name: 'branch', group: 'git', types: ['string'] },
{ name: 'commitEntry', group: 'git', types: ['string'] },
{ name: 'commitMessage', group: 'git', types: ['string'] },
{ name: 'commitSha', group: 'git', types: ['string'] },
{ name: 'shortSha', group: 'git', types: ['string'] },
{ name: 'abbreviation', group: 'hacker', types: ['string'] },
{ name: 'adjective', group: 'hacker', types: ['string'] },
{ name: 'noun', group: 'hacker', types: ['string'] },
{ name: 'verb', group: 'hacker', types: ['string'] },
{ name: 'ingverb', group: 'hacker', types: ['string'] },
{ name: 'phrase', group: 'hacker', types: ['string'] },
// { name: 'avatar', group: 'internet', types: ['string'] },
{ name: 'email', group: 'internet', types: ['string'] },
{ name: 'exampleEmail', group: 'internet', types: ['string'] },
{ name: 'userName', group: 'internet', types: ['string'] },
{ name: 'protocol', group: 'internet', types: ['string'] },
{ name: 'url', group: 'internet', types: ['string'] },
{ name: 'domainName', group: 'internet', types: ['string'] },
{ name: 'domainSuffix', group: 'internet', types: ['string'] },
{ name: 'domainWord', group: 'internet', types: ['string'] },
{ name: 'ip', group: 'internet', types: ['string'] },
{ name: 'ipv6', group: 'internet', types: ['string'] },
{ name: 'userAgent', group: 'internet', types: ['string'] },
{ name: 'color', group: 'internet', types: ['string'] },
{ name: 'mac', group: 'internet', types: ['string'] },
{ name: 'password', group: 'internet', types: ['string'] },
{ name: 'word', group: 'lorem', types: ['string'] },
{ name: 'words', group: 'lorem', types: ['string'] },
{ name: 'sentence', group: 'lorem', types: ['string'] },
{ name: 'slug', group: 'lorem', types: ['string'] },
{ name: 'sentences', group: 'lorem', types: ['string'] },
{ name: 'paragraph', group: 'lorem', types: ['string'] },
{ name: 'paragraphs', group: 'lorem', types: ['string'] },
{ name: 'text', group: 'lorem', types: ['string'] },
{ name: 'lines', group: 'lorem', types: ['string'] },
{ name: 'genre', group: 'music', types: ['string'] },
{ name: 'firstName', group: 'name', types: ['string'] },
{ name: 'lastName', group: 'name', types: ['string'] },
{ name: 'middleName', group: 'name', types: ['string'] },
{ name: 'findName', group: 'name', types: ['string'] },
{ name: 'jobTitle', group: 'name', types: ['string'] },
{ name: 'gender', group: 'name', types: ['string'] },
{ name: 'prefix', group: 'name', types: ['string'] },
{ name: 'suffix', group: 'name', types: ['string'] },
{ name: 'title', group: 'name', types: ['string'] },
{ name: 'jobDescriptor', group: 'name', types: ['string'] },
{ name: 'jobArea', group: 'name', types: ['string'] },
{ name: 'jobType', group: 'name', types: ['string'] },
{ name: 'phoneNumber', group: 'phone', types: ['string'] },
{ name: 'phoneNumberFormat', group: 'phone', types: ['string'] },
{ name: 'phoneFormats', group: 'phone', types: ['string'] },
{ name: 'number', group: 'random', types: ['string', 'number'], params: ['min', 'max'] },
{ name: 'float', group: 'random', types: ['string', 'float'], params: ['min', 'max'] },
{ name: 'arrayElement', group: 'random', types: ['string'] },
{ name: 'arrayElements', group: 'random', types: ['string'] },
{ name: 'objectElement', group: 'random', types: ['string'] },
{ name: 'uuid', group: 'random', types: ['string'] },
{ name: 'boolean', group: 'random', types: ['string'] },
{ name: 'word', group: 'random', types: ['string'] },
{ name: 'words', group: 'random', types: ['string'] },
// { name: 'image', group: 'random', types: ['string'] },
{ name: 'locale', group: 'random', types: ['string'] },
{ name: 'alpha', group: 'random', types: ['string'] },
{ name: 'alphaNumeric', group: 'random', types: ['string'] },
{ name: 'hexaDecimal', group: 'random', types: ['string'] },
{ name: 'fileName', group: 'system', types: ['string'] },
{ name: 'commonFileName', group: 'system', types: ['string'] },
{ name: 'mimeType', group: 'system', types: ['string'] },
{ name: 'commonFileType', group: 'system', types: ['string'] },
{ name: 'commonFileExt', group: 'system', types: ['string'] },
{ name: 'fileType', group: 'system', types: ['string'] },
{ name: 'fileExt', group: 'system', types: ['string'] },
{ name: 'directoryPath', group: 'system', types: ['string'] },
{ name: 'filePath', group: 'system', types: ['string'] },
{ name: 'semver', group: 'system', types: ['string'] },
{ name: 'recent', group: 'time', types: ['string', 'time'] },
{ name: 'vehicle', group: 'vehicle', types: ['string'] },
{ name: 'manufacturer', group: 'vehicle', types: ['string'] },
{ name: 'model', group: 'vehicle', types: ['string'] },
{ name: 'type', group: 'vehicle', types: ['string'] },
{ name: 'fuel', group: 'vehicle', types: ['string'] },
{ name: 'vin', group: 'vehicle', types: ['string'] },
{ name: 'color', group: 'vehicle', types: ['string'] }
];
}
static getGroups () {
const groupsObj = this._methods.reduce((acc, curr) => {
if (curr.group in acc)
curr.types.forEach(type => acc[curr.group].add(type));
else
acc[curr.group] = new Set(curr.types);
return acc;
}, {});
const groupsArr = [];
for (const key in groupsObj)
groupsArr.push({ name: key, types: [...groupsObj[key]] });
return groupsArr.sort((a, b) => {
if (a.name < b.name)
return -1;
if (b.name > a.name)
return 1;
return 0;
}); ;
}
static getGroupsByType (type) {
if (!type) return [];
return this.getGroups().filter(group => group.types.includes(type));
}
static getMethods ({ type, group }) {
return this._methods.filter(method => method.group === group && method.types.includes(type)).sort((a, b) => {
if (a.name < b.name)
return -1;
if (b.name > a.name)
return 1;
return 0;
});
}
}

View File

@@ -0,0 +1,41 @@
module.exports = {
// Defaults
defaultPort: null,
defaultUser: null,
defaultDatabase: null,
// Core
database: false,
collations: false,
engines: false,
// Tools
processesList: false,
usersManagement: false,
variables: false,
// Structure
schemas: false,
tables: false,
views: false,
triggers: false,
routines: false,
functions: false,
schedulers: false,
// Settings
tableAdd: false,
viewAdd: false,
triggerAdd: false,
routineAdd: false,
functionAdd: false,
schedulerAdd: false,
databaseEdit: false,
schemaEdit: false,
tableSettings: false,
viewSettings: false,
triggerSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
indexes: false,
foreigns: false,
sortableFields: false,
zerofill: false
};

View File

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

View File

@@ -0,0 +1,40 @@
const defaults = require('./defaults');
module.exports = {
...defaults,
// Defaults
defaultPort: 3306,
defaultUser: 'root',
defaultDatabase: null,
// Core
collations: true,
engines: true,
// Tools
processesList: true,
// Structure
schemas: true,
tables: true,
views: true,
triggers: true,
routines: true,
functions: true,
schedulers: true,
// Settings
tableAdd: true,
viewAdd: true,
triggerAdd: true,
routineAdd: true,
functionAdd: true,
schedulerAdd: true,
schemaEdit: true,
tableSettings: true,
viewSettings: true,
triggerSettings: true,
routineSettings: true,
functionSettings: true,
schedulerSettings: true,
indexes: true,
foreigns: true,
sortableFields: true,
zerofill: true
};

View File

@@ -0,0 +1,31 @@
const defaults = require('./defaults');
module.exports = {
...defaults,
// Defaults
defaultPort: 5432,
defaultUser: 'postgres',
defaultDatabase: 'postgres',
// Core
database: true,
// Tools
processesList: true,
// Structure
tables: true,
views: false,
triggers: false,
routines: false,
functions: false,
schedulers: false,
// Settings
databaseEdit: false,
tableSettings: false,
viewSettings: false,
triggerSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
indexes: true,
foreigns: true,
sortableFields: false
};

View File

@@ -91,7 +91,7 @@ module.exports = [
},
{
name: 'TINYTEXT',
length: true,
length: false,
collation: true,
unsigned: false,
zerofill: false
@@ -119,7 +119,7 @@ module.exports = [
},
{
name: 'JSON',
length: true,
length: false,
collation: true,
unsigned: false,
zerofill: false
@@ -279,7 +279,7 @@ module.exports = [
types: [
{
name: 'UNKNOWN',
length: true,
length: false,
collation: false,
unsigned: false,
zerofill: false

View File

@@ -0,0 +1,297 @@
module.exports = [
{
group: 'integer',
types: [
{
name: 'SMALLINT',
length: true,
unsigned: true
},
{
name: 'INTEGER',
length: true,
unsigned: true
},
{
name: 'BIGINT',
length: true,
unsigned: true
},
{
name: 'DECIMAL',
length: true,
unsigned: true
},
{
name: 'NUMERIC',
length: true,
unsigned: true
},
{
name: 'SMALLSERIAL',
length: true,
unsigned: true
},
{
name: 'SERIAL',
length: true,
unsigned: true
},
{
name: 'BIGSERIAL',
length: true,
unsigned: true
}
]
},
{
group: 'float',
types: [
{
name: 'REAL',
length: true,
unsigned: true
},
{
name: 'DOUBLE PRECISION',
length: true,
unsigned: true
}
]
},
{
group: 'monetary',
types: [
{
name: 'money',
length: true,
unsigned: true
}
]
},
{
group: 'string',
types: [
{
name: 'CHARACTER VARYING',
length: true,
unsigned: false
},
{
name: 'CHAR',
length: false,
unsigned: false
},
{
name: 'CHARACTER',
length: false,
unsigned: false
},
{
name: 'TEXT',
length: false,
unsigned: false
},
{
name: '"CHAR"',
length: false,
unsigned: false
},
{
name: 'NAME',
length: false,
unsigned: false
}
]
},
{
group: 'binary',
types: [
{
name: 'BYTEA',
length: true,
unsigned: false
}
]
},
{
group: 'time',
types: [
{
name: 'TIMESTAMP WITHOUT TIME ZONE',
length: false,
unsigned: false
},
{
name: 'TIMESTAMP WITH TIME ZONE',
length: false,
unsigned: false
},
{
name: 'DATE',
length: true,
unsigned: false
},
{
name: 'TIME',
length: true,
unsigned: false
},
{
name: 'TIME WITH TIME ZONE',
length: true,
unsigned: false
},
{
name: 'INTERVAL',
length: false,
unsigned: false
}
]
},
{
group: 'boolean',
types: [
{
name: 'BOOLEAN',
length: false,
unsigned: false
}
]
},
{
group: 'geometric',
types: [
{
name: 'POINT',
length: false,
unsigned: false
},
{
name: 'LINE',
length: false,
unsigned: false
},
{
name: 'LSEG',
length: false,
unsigned: false
},
{
name: 'BOX',
length: false,
unsigned: false
},
{
name: 'PATH',
length: false,
unsigned: false
},
{
name: 'POLYGON',
length: false,
unsigned: false
},
{
name: 'CIRCLE',
length: false,
unsigned: false
}
]
},
{
group: 'network',
types: [
{
name: 'CIDR',
length: false,
unsigned: false
},
{
name: 'INET',
length: false,
unsigned: false
},
{
name: 'MACADDR',
length: false,
unsigned: false
},
{
name: 'MACADDR8',
length: false,
unsigned: false
}
]
},
{
group: 'bit',
types: [
{
name: 'BIT',
length: false,
unsigned: false
},
{
name: 'BIT VARYING',
length: false,
unsigned: false
}
]
},
{
group: 'text search',
types: [
{
name: 'TSVECTOR',
length: false,
unsigned: false
},
{
name: 'TSQUERY',
length: false,
unsigned: false
}
]
},
{
group: 'uuid',
types: [
{
name: 'UUID',
length: false,
unsigned: false
}
]
},
{
group: 'xml',
types: [
{
name: 'XML',
length: false,
unsigned: false
}
]
},
{
group: 'json',
types: [
{
name: 'JSON',
length: false,
unsigned: false
},
{
name: 'JSONB',
length: false,
unsigned: false
},
{
name: 'JSONPATH',
length: false,
unsigned: false
}
]
}
];

View File

@@ -1,12 +1,75 @@
export const TEXT = ['CHAR', 'VARCHAR'];
export const LONG_TEXT = ['TEXT', 'MEDIUMTEXT', 'LONGTEXT'];
export const TEXT = [
'CHAR',
'VARCHAR',
'CHARACTER',
'CHARACTER VARYING'
];
export const LONG_TEXT = [
'TEXT',
'MEDIUMTEXT',
'LONGTEXT'
];
export const NUMBER = ['INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL', 'BOOL'];
export const ARRAY = [
'ARRAY',
'ANYARRAY'
];
export const TEXT_SEARCH = [
'TSVECTOR',
'TSQUERY'
];
export const NUMBER = [
'INT',
'TINYINT',
'SMALLINT',
'MEDIUMINT',
'BIGINT',
'DECIMAL',
'NUMERIC',
'INTEGER',
'SMALLSERIAL',
'SERIAL',
'BIGSERIAL',
'OID',
'XID'
];
export const FLOAT = [
'FLOAT',
'DOUBLE',
'REAL',
'DOUBLE PRECISION',
'MONEY'
];
export const BOOLEAN = [
'BOOL',
'BOOLEAN'
];
export const DATE = ['DATE'];
export const TIME = ['TIME'];
export const DATETIME = ['DATETIME', 'TIMESTAMP'];
export const TIME = [
'TIME',
'TIME WITH TIME ZONE'
];
export const DATETIME = [
'DATETIME',
'TIMESTAMP',
'TIMESTAMP WITHOUT TIME ZONE',
'TIMESTAMP WITH TIME ZONE'
];
export const BLOB = ['BLOB', 'MEDIUMBLOB', 'LONGBLOB'];
export const BLOB = [
'BLOB',
'TINYBLOB',
'MEDIUMBLOB',
'LONGBLOB',
'BYTEA'
];
export const BIT = ['BIT'];
export const BIT = [
'BIT',
'BIT VARYING'
];

View File

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

View File

@@ -5,8 +5,12 @@ import * as path from 'path';
import crypto from 'crypto';
import { format as formatUrl } from 'url';
import keytar from 'keytar';
import Store from 'electron-store';
import ipcHandlers from './ipc-handlers';
Store.initRenderer();
const isDevelopment = process.env.NODE_ENV !== 'production';
const gotTheLock = app.requestSingleInstanceLock();
@@ -92,11 +96,16 @@ else {
// create main BrowserWindow when electron is ready
app.on('ready', async () => {
let key = await keytar.getPassword('antares', 'user');
try {
let key = await keytar.getPassword('antares', 'user');
if (!key) {
key = crypto.randomBytes(16).toString('hex');
keytar.setPassword('antares', 'user', key);
if (!key) {
key = crypto.randomBytes(16).toString('hex');
keytar.setPassword('antares', 'user', key);
}
}
catch (err) {
console.log(err);
}
mainWindow = createMainWindow();

View File

@@ -7,7 +7,14 @@ export default () => {
});
ipcMain.on('get-key', async event => {
const key = await keytar.getPassword('antares', 'user');
let key = false;
try {
key = await keytar.getPassword('antares', 'user');
}
catch (err) {
console.log(err);
}
event.returnValue = key;
});
};

View File

@@ -1,17 +1,31 @@
import fs from 'fs';
import { ipcMain } from 'electron';
import { ClientsFactory } from '../libs/ClientsFactory';
export default connections => {
ipcMain.handle('test-connection', async (event, conn) => {
const params = {
host: conn.host,
port: +conn.port,
user: conn.user,
password: conn.password
};
if (conn.database)
params.database = conn.database;
if (conn.ssl) {
params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null,
cert: conn.cert ? fs.readFileSync(conn.cert) : null,
ca: conn.ca ? fs.readFileSync(conn.ca) : null,
ciphers: conn.ciphers
};
}
const connection = ClientsFactory.getConnection({
client: conn.client,
params: {
host: conn.host,
port: +conn.port,
user: conn.user,
password: conn.password
}
params
});
await connection.connect();
@@ -32,21 +46,35 @@ export default connections => {
});
ipcMain.handle('connect', async (event, conn) => {
const connection = ClientsFactory.getConnection({
client: conn.client,
params: {
host: conn.host,
port: +conn.port,
user: conn.user,
password: conn.password
},
poolSize: 1
});
const params = {
host: conn.host,
port: +conn.port,
user: conn.user,
password: conn.password
};
if (conn.database)
params.database = conn.database;
if (conn.ssl) {
params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null,
cert: conn.cert ? fs.readFileSync(conn.cert) : null,
ca: conn.ca ? fs.readFileSync(conn.ca) : null,
ciphers: conn.ciphers
};
}
try {
const connection = ClientsFactory.getConnection({
client: conn.client,
params,
poolSize: 1
});
await connection.connect();
const structure = await connection.getStructure();
const structure = await connection.getStructure(new Set());
connections[conn.uid] = connection;

View File

@@ -0,0 +1,43 @@
import { ipcMain } from 'electron';
export default (connections) => {
ipcMain.handle('get-function-informations', async (event, params) => {
try {
const result = await connections[params.uid].getFunctionInformations(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('drop-function', async (event, params) => {
try {
await connections[params.uid].dropFunction(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('alter-function', async (event, params) => {
try {
await connections[params.uid].alterFunction(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('create-function', async (event, params) => {
try {
await connections[params.uid].createFunction(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -3,9 +3,11 @@ import tables from './tables';
import views from './views';
import triggers from './triggers';
import routines from './routines';
import functions from './functions';
import schedulers from './schedulers';
import updates from './updates';
import application from './application';
import database from './database';
import schema from './schema';
import users from './users';
const connections = {};
@@ -16,7 +18,9 @@ export default () => {
views(connections);
triggers(connections);
routines(connections);
database(connections);
functions(connections);
schedulers(connections);
schema(connections);
users(connections);
updates();
application();

View File

@@ -0,0 +1,43 @@
import { ipcMain } from 'electron';
export default (connections) => {
ipcMain.handle('get-scheduler-informations', async (event, params) => {
try {
const result = await connections[params.uid].getEventInformations(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('drop-scheduler', async (event, params) => {
try {
await connections[params.uid].dropEvent(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('alter-scheduler', async (event, params) => {
try {
await connections[params.uid].alterEvent(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('create-scheduler', async (event, params) => {
try {
await connections[params.uid].createEvent(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -2,10 +2,9 @@
import { ipcMain } from 'electron';
export default connections => {
ipcMain.handle('create-database', async (event, params) => {
ipcMain.handle('create-schema', async (event, params) => {
try {
const query = `CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`;
await connections[params.uid].raw(query);
await connections[params.uid].createSchema(params);
return { status: 'success' };
}
@@ -14,10 +13,9 @@ export default connections => {
}
});
ipcMain.handle('update-database', async (event, params) => {
ipcMain.handle('update-schema', async (event, params) => {
try {
const query = `ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`;
await connections[params.uid].raw(query);
await connections[params.uid].alterSchema(params);
return { status: 'success' };
}
@@ -26,10 +24,9 @@ export default connections => {
}
});
ipcMain.handle('delete-database', async (event, params) => {
ipcMain.handle('delete-schema', async (event, params) => {
try {
const query = `DROP DATABASE \`${params.database}\``;
await connections[params.uid].raw(query);
await connections[params.uid].dropSchema(params);
return { status: 'success' };
}
@@ -38,10 +35,9 @@ export default connections => {
}
});
ipcMain.handle('get-database-collation', async (event, params) => { // TODO: move to mysql class
ipcMain.handle('get-schema-collation', async (event, params) => {
try {
const query = `SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`;
const collation = await connections[params.uid].raw(query);
const collation = await connections[params.uid].getDatabaseCollation(params);
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
}
@@ -50,9 +46,9 @@ export default connections => {
}
});
ipcMain.handle('get-structure', async (event, uid) => {
ipcMain.handle('get-structure', async (event, params) => {
try {
const structure = await connections[uid].getStructure();
const structure = await connections[params.uid].getStructure(params.schemas);
return { status: 'success', response: structure };
}
@@ -94,6 +90,28 @@ export default connections => {
}
});
ipcMain.handle('get-version', async (event, uid) => {
try {
const result = await connections[uid].getVersion();
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-processes', async (event, uid) => {
try {
const result = await connections[uid].getProcesses();
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('use-schema', async (event, { uid, schema }) => {
if (!schema) return;

View File

@@ -1,6 +1,8 @@
import { ipcMain } from 'electron';
import faker from 'faker';
import moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import fs from 'fs';
export default (connections) => {
@@ -57,33 +59,94 @@ export default (connections) => {
});
ipcMain.handle('update-table-cell', async (event, params) => {
try {
try { // TODO: move to client classes
let escapedParam;
let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
if (NUMBER.includes(params.type))
if ([...NUMBER, ...FLOAT].includes(params.type))
escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type))
escapedParam = `"${sqlEscaper(params.content)}"`;
else if ([...TEXT, ...LONG_TEXT].includes(params.type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.content)}"`;
break;
case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (ARRAY.includes(params.type))
escapedParam = `'${params.content}'`;
else if (TEXT_SEARCH.includes(params.type))
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
else if (BLOB.includes(params.type)) {
if (params.content) {
const fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
reload = true;
}
else
escapedParam = '""';
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '\'\'';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
}
else if ([...BIT].includes(params.type)) {
escapedParam = `b'${sqlEscaper(params.content)}'`;
reload = true;
}
else if (params.content === null)
escapedParam = 'NULL';
else
escapedParam = `"${sqlEscaper(params.content)}"`;
escapedParam = `'${sqlEscaper(params.content)}'`;
await connections[params.uid]
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${id}` })
.run();
if (params.primary) { // TODO: handle multiple primary
await connections[params.uid]
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${id}` })
.limit(1)
.run();
}
else {
const { orgRow } = params;
reload = true;
for (const key in orgRow) {
if (typeof orgRow[key] === 'string')
orgRow[key] = `'${orgRow[key]}'`;
orgRow[key] = `= ${orgRow[key]}`;
}
await connections[params.uid]
.schema(params.schema)
.update({ [params.field]: `= ${escapedParam}` })
.from(params.table)
.where(orgRow)
.limit(1)
.run();
}
return { status: 'success', response: { reload } };
}
@@ -93,29 +156,52 @@ export default (connections) => {
});
ipcMain.handle('delete-table-rows', async (event, params) => {
let idString;
if (params.primary) {
const idString = params.rows.map(row => typeof row[params.primary] === 'string'
? `"${row[params.primary]}"`
: row[params.primary]).join(',');
if (typeof params.rows[0] === 'string')
idString = params.rows.map(row => `"${row}"`).join(',');
else
idString = params.rows.join(',');
try {
const result = await connections[params.uid]
.schema(params.schema)
.delete(params.table)
.where({ [params.primary]: `IN (${idString})` })
.run();
try {
const result = await connections[params.uid]
.schema(params.schema)
.delete(params.table)
.where({ [params.primary]: `IN (${idString})` })
.run();
return { status: 'success', response: result };
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
}
catch (err) {
return { status: 'error', response: err.toString() };
else {
try {
for (const row of params.rows) {
for (const key in row) {
if (typeof row[key] === 'string')
row[key] = `'${row[key]}'`;
row[key] = `= ${row[key]}`;
}
await connections[params.uid]
.schema(params.schema)
.delete(params.table)
.where(row)
.limit(1)
.run();
}
return { status: 'success', response: [] };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
}
});
ipcMain.handle('insert-table-rows', async (event, params) => {
try {
try { // TODO: move to client classes
const insertObj = {};
for (const key in params.row) {
const type = params.fields[key];
@@ -123,32 +209,166 @@ export default (connections) => {
if (params.row[key] === null)
escapedParam = 'NULL';
else if (NUMBER.includes(type))
escapedParam = params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type))
escapedParam = `"${sqlEscaper(params.row[key])}"`;
else if (BLOB.includes(type)) {
if (params.row[key]) {
const fileBlob = fs.readFileSync(params.row[key]);
escapedParam = `0x${fileBlob.toString('hex')}`;
else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = +params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break;
case 'pg':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (BLOB.includes(type)) {
if (params.row[key].value) {
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
}
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '""';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
else
escapedParam = '""';
}
else
escapedParam = `"${sqlEscaper(params.row[key])}"`;
insertObj[key] = escapedParam;
}
for (let i = 0; i < params.repeat; i++) {
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(insertObj)
.run();
const rows = new Array(+params.repeat).fill(insertObj);
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(rows)
.run();
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('insert-table-fake-rows', async (event, params) => {
try { // TODO: move to client classes
const rows = [];
for (let i = 0; i < +params.repeat; i++) {
const insertObj = {};
for (const key in params.row) {
const type = params.fields[key];
let escapedParam;
if (!('group' in params.row[key]) || params.row[key].group === 'manual') { // Manual value
if (params.row[key].value === null || params.row[key].value === undefined)
escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = params.row[key].value;
else if ([...TEXT, ...LONG_TEXT].includes(type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break;
case 'pg':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (BLOB.includes(type)) {
if (params.row[key].value) {
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
}
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '""';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
}
else if (BIT.includes(type))
escapedParam = `b'${sqlEscaper(params.row[key].value)}'`;
else
escapedParam = `'${sqlEscaper(params.row[key].value)}'`;
insertObj[key] = escapedParam;
}
else { // Faker value
const parsedParams = {};
let fakeValue;
if (params.locale)
faker.locale = params.locale;
if (Object.keys(params.row[key].params).length) {
Object.keys(params.row[key].params).forEach(param => {
if (!isNaN(params.row[key].params[param]))
parsedParams[param] = +params.row[key].params[param];
});
fakeValue = faker[params.row[key].group][params.row[key].method](parsedParams);
}
else
fakeValue = faker[params.row[key].group][params.row[key].method]();
if (typeof fakeValue === 'string') {
if (params.row[key].length)
fakeValue = fakeValue.substr(0, params.row[key].length);
fakeValue = `'${sqlEscaper(fakeValue)}'`;
}
else if ([...DATE, ...DATETIME].includes(type))
fakeValue = `'${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}'`;
insertObj[key] = fakeValue;
}
}
rows.push(insertObj);
}
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(rows)
.run();
return { status: 'success' };
}
catch (err) {
@@ -159,13 +379,13 @@ export default (connections) => {
ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => {
try {
const query = connections[uid]
.select(`${column} AS foreignColumn`)
.select(`${column} AS foreign_column`)
.schema(schema)
.from(table)
.orderBy('foreignColumn ASC');
.orderBy('foreign_column ASC');
if (description)
query.select(`LEFT(${description}, 20) AS foreignDescription`);
query.select(`LEFT(${description}, 20) AS foreign_description`);
const results = await query.run();

View File

@@ -1,8 +1,10 @@
import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
let mainWindow;
autoUpdater.allowPrerelease = true;
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
export default () => {
ipcMain.on('check-for-updates', event => {

View File

@@ -28,7 +28,7 @@ export class AntaresCore {
limit: [],
join: [],
update: [],
insert: {},
insert: [],
delete: false
};
this._query = Object.assign({}, this._queryDefaults);
@@ -120,12 +120,12 @@ export class AntaresCore {
}
/**
* @param {Object} obj field: value
* @param {Array} arr Array of row objects
* @returns
* @memberof AntaresCore
*/
insert (obj) {
this._query.insert = { ...this._query.insert, ...obj };
insert (arr) {
this._query.insert = [...this._query.insert, ...arr];
return this;
}

View File

@@ -1,5 +1,6 @@
'use strict';
import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
export class ClientsFactory {
/**
@@ -20,8 +21,10 @@ export class ClientsFactory {
case 'mysql':
case 'maria':
return new MySQLClient(args);
case 'pg':
return new PostgreSQLClient(args);
default:
return new Error(`Unknown database client: ${args.client}`);
throw new Error(`Unknown database client: ${args.client}`);
}
}
}

View File

@@ -1,8 +1,101 @@
'use strict';
import mysql from 'mysql';
import mysql from 'mysql2';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql';
export class MySQLClient extends AntaresCore {
constructor (args) {
super(args);
this.types = {
0: 'DECIMAL',
1: 'TINYINT',
2: 'SMALLINT',
3: 'INT',
4: 'FLOAT',
5: 'DOUBLE',
6: 'NULL',
7: 'TIMESTAMP',
8: 'BIGINT',
9: 'MEDIUMINT',
10: 'DATE',
11: 'TIME',
12: 'DATETIME',
13: 'YEAR',
14: 'NEWDATE',
15: 'VARCHAR',
16: 'BIT',
17: 'TIMESTAMP2',
18: 'DATETIME2',
19: 'TIME2',
245: 'JSON',
246: 'NEWDECIMAL',
247: 'ENUM',
248: 'SET',
249: 'TINY_BLOB',
250: 'MEDIUM_BLOB',
251: 'LONG_BLOB',
252: 'BLOB',
253: 'VARCHAR',
254: 'CHAR',
255: 'GEOMETRY'
};
}
_getType (field) {
let name = this.types[field.columnType];
let length = field.columnLength;
if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name))
length = field.decimals;
if (name === 'TIMESTAMP')
length = 0;
if (field.charsetNr === 63) { // if binary
if (name === 'CHAR')
name = 'BINARY';
else if (name === 'VARCHAR')
name = 'VARBINARY';
}
if (name === 'BLOB') {
switch (length) {
case 765:
name = 'TYNITEXT';
break;
case 196605:
name = 'TEXT';
break;
case 50331645:
name = 'MEDIUMTEXT';
break;
case 4294967295:
name = field.charsetNr === 63 ? 'LONGBLOB' : 'LONGTEXT';
break;
case 255:
name = 'TINYBLOB';
break;
case 65535:
name = 'BLOB';
break;
case 16777215:
name = 'MEDIUMBLOB';
break;
default:
name = field.charsetNr === 63 ? 'BLOB' : 'TEXT';
}
}
return { name, length };
}
_getTypeInfo (type) {
return dataTypes
.reduce((acc, group) => [...acc, ...group.types], [])
.filter(_type => _type.name === type.toUpperCase())[0];
}
/**
* @memberof MySQLClient
*/
@@ -31,10 +124,11 @@ export class MySQLClient extends AntaresCore {
}
/**
* @param {Array} schemas list
* @returns {Array.<Object>} databases scructure
* @memberof MySQLClient
*/
async getStructure () {
async getStructure (schemas) {
const { rows: databases } = await this.raw('SHOW DATABASES');
const { rows: functions } = await this.raw('SHOW FUNCTION STATUS');
const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS');
@@ -44,6 +138,8 @@ export class MySQLClient extends AntaresCore {
const triggersArr = [];
for (const db of databases) {
if (!schemas.has(db.Database)) continue;
let { rows: tables } = await this.raw(`SHOW TABLE STATUS FROM \`${db.Database}\``);
if (tables.length) {
tables = tables.map(table => {
@@ -64,109 +160,121 @@ export class MySQLClient extends AntaresCore {
}
return databases.map(db => {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => {
let tableType;
switch (table.Comment) {
case 'VIEW':
tableType = 'view';
break;
default:
tableType = 'table';
break;
}
if (schemas.has(db.Database)) {
// TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => {
let tableType;
switch (table.Comment) {
case 'VIEW':
tableType = 'view';
break;
default:
tableType = 'table';
break;
}
return {
name: table.Name,
type: tableType,
rows: table.Rows,
created: table.Create_time,
updated: table.Update_time,
engine: table.Engine,
comment: table.Comment,
size: table.Data_length + table.Index_length,
autoIncrement: table.Auto_increment,
collation: table.Collation
};
});
// PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
return {
name: procedure.Name,
type: procedure.Type,
definer: procedure.Definer,
created: procedure.Created,
updated: procedure.Modified,
comment: procedure.Comment,
charset: procedure.character_set_client,
security: procedure.Security_type
};
});
// FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.Database).map(func => {
return {
name: func.Name,
type: func.Type,
definer: func.Definer,
created: func.Created,
updated: func.Modified,
comment: func.Comment,
charset: func.character_set_client,
security: func.Security_type
};
});
// SCHEDULERS
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return {
name: scheduler.EVENT_NAME,
definition: scheduler.EVENT_DEFINITION,
type: scheduler.EVENT_TYPE,
definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS,
ends: scheduler.ENDS,
status: scheduler.STATUS,
executeAt: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD,
intervalValue: scheduler.INTERVAL_VALUE,
onCompletion: scheduler.ON_COMPLETION,
originator: scheduler.ORIGINATOR,
sqlMode: scheduler.SQL_MODE,
created: scheduler.CREATED,
updated: scheduler.LAST_ALTERED,
lastExecuted: scheduler.LAST_EXECUTED,
comment: scheduler.EVENT_COMMENT,
charset: scheduler.CHARACTER_SET_CLIENT,
timezone: scheduler.TIME_ZONE
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => {
return {
name: trigger.Trigger,
statement: trigger.Statement,
timing: trigger.Timing,
definer: trigger.Definer,
event: trigger.Event,
table: trigger.Table,
sqlMode: trigger.sql_mode,
created: trigger.Created,
charset: trigger.character_set_client
};
});
return {
name: table.Name,
type: tableType,
rows: table.Rows,
created: table.Create_time,
updated: table.Update_time,
engine: table.Engine,
comment: table.Comment,
size: table.Data_length + table.Index_length,
autoIncrement: table.Auto_increment,
collation: table.Collation
name: db.Database,
tables: remappedTables,
functions: remappedFunctions,
procedures: remappedProcedures,
triggers: remappedTriggers,
schedulers: remappedSchedulers
};
});
// PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
}
else {
return {
name: procedure.Name,
type: procedure.Type,
definer: procedure.Definer,
created: procedure.Created,
updated: procedure.Modified,
comment: procedure.Comment,
charset: procedure.character_set_client,
security: procedure.Security_type
name: db.Database,
tables: [],
functions: [],
procedures: [],
triggers: [],
schedulers: []
};
});
// FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.Database).map(func => {
return {
name: func.Name,
type: func.Type,
definer: func.Definer,
created: func.Created,
updated: func.Modified,
comment: func.Comment,
charset: func.character_set_client,
security: func.Security_type
};
});
// SCHEDULERS
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return {
name: scheduler.EVENT_NAME,
definition: scheduler.EVENT_DEFINITION,
type: scheduler.EVENT_TYPE,
definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS,
ends: scheduler.ENDS,
status: scheduler.STATUS,
executeAt: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD,
intervalValue: scheduler.INTERVAL_VALUE,
onCompletion: scheduler.ON_COMPLETION,
originator: scheduler.ORIGINATOR,
sqlMode: scheduler.SQL_MODE,
created: scheduler.CREATED,
updated: scheduler.LAST_ALTERED,
lastExecuted: scheduler.LAST_EXECUTED,
comment: scheduler.EVENT_COMMENT,
charset: scheduler.CHARACTER_SET_CLIENT,
timezone: scheduler.TIME_ZONE
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => {
return {
name: trigger.Trigger,
statement: trigger.Statement,
timing: trigger.Timing,
definer: trigger.Definer,
event: trigger.Event,
table: trigger.Table,
sqlMode: trigger.sql_mode,
created: trigger.Created,
charset: trigger.character_set_client
};
});
return {
name: db.Database,
tables: remappedTables,
functions: remappedFunctions,
procedures: remappedProcedures,
triggers: remappedTriggers,
schedulers: remappedSchedulers
};
}
});
}
@@ -279,13 +387,13 @@ export class MySQLClient extends AntaresCore {
}
/**
* SELECT `user`, `host`, IF(LENGTH(password)>0, password, authentication_string) AS `password` FROM `mysql`.`user`
* SELECT `user`, `host`, authentication_string) AS `password` FROM `mysql`.`user`
*
* @returns {Array.<Object>} users list
* @memberof MySQLClient
*/
async getUsers () {
const { rows } = await this.raw('SELECT `user`, `host`, IF(LENGTH(password)>0, password, authentication_string) AS `password` FROM `mysql`.`user`');
const { rows } = await this.raw('SELECT `user`, `host`, authentication_string AS `password` FROM `mysql`.`user`');
return rows.map(row => {
return {
@@ -296,6 +404,44 @@ export class MySQLClient extends AntaresCore {
});
}
/**
* CREATE DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createSchema (params) {
return await this.raw(`CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`);
}
/**
* ALTER DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterSchema (params) {
return await this.raw(`ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`);
}
/**
* DROP DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropSchema (params) {
return await this.raw(`DROP DATABASE \`${params.database}\``);
}
/**
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async getDatabaseCollation (params) {
return await this.raw(`SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`);
}
/**
* SHOW CREATE VIEW
*
@@ -433,19 +579,32 @@ export class MySQLClient extends AntaresCore {
const results = await this.raw(sql);
return results.rows.map(row => {
if (!row['Create Procedure']) {
return {
definer: null,
sql: '',
parameters: [],
name: row.Procedure,
comment: '',
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
};
}
const parameters = row['Create Procedure']
.match(/(?<=\().*?(?=\))/s)[0]
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null];
return {
name: param[1] ? param[1].replaceAll('`', '') : '',
type: type[0],
length: +type[1],
type: type[0].replaceAll('\n', ''),
length: +type[1] ? +type[1].replace(/\D/g, '') : '',
context: param[0] ? param[0].replace('\n', '') : ''
};
}).filter(el => el.name);
@@ -461,7 +620,7 @@ export class MySQLClient extends AntaresCore {
return {
definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters,
parameters: parameters || [],
name: row.Procedure,
comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
@@ -511,10 +670,12 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async createRoutine (routine) {
const parameters = routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
const parameters = 'parameters' in routine
? routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',')
: '';
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters})
LANGUAGE SQL
@@ -527,6 +688,221 @@ export class MySQLClient extends AntaresCore {
return await this.raw(sql, { split: false });
}
/**
* SHOW CREATE FUNCTION
*
* @returns {Array.<Object>} view informations
* @memberof MySQLClient
*/
async getFunctionInformations ({ schema, func }) {
const sql = `SHOW CREATE FUNCTION \`${schema}\`.\`${func}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
if (!row['Create Function']) {
return {
definer: null,
sql: '',
parameters: [],
name: row.Procedure,
comment: '',
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL',
returns: 'INT',
returnsLength: null
};
}
const parameters = row['Create Function']
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[1] ? param[1].replace(')', '').split('(') : ['', null];
return {
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0],
length: +type[1] ? +type[1].replace(/\D/g, '') : ''
};
}).filter(el => el.name);
let dataAccess = 'CONTAINS SQL';
if (row['Create Function'].includes('NO SQL'))
dataAccess = 'NO SQL';
if (row['Create Function'].includes('READS SQL DATA'))
dataAccess = 'READS SQL DATA';
if (row['Create Function'].includes('MODIFIES SQL DATA'))
dataAccess = 'MODIFIES SQL DATA';
const output = row['Create Function'].match(/(?<=RETURNS ).*?(?=\s)/gs).length ? row['Create Function'].match(/(?<=RETURNS ).*?(?=\s)/gs)[0].replace(')', '').split('(') : ['', null];
return {
definer: row['Create Function'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Function'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters: parameters || [],
name: row.Function,
comment: row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Function'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
deterministic: row['Create Function'].includes('DETERMINISTIC'),
dataAccess,
returns: output[0].toUpperCase(),
returnsLength: +output[1]
};
})[0];
}
/**
* DROP FUNCTION
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropFunction (params) {
const sql = `DROP FUNCTION \`${params.func}\``;
return await this.raw(sql);
}
/**
* ALTER FUNCTION
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterFunction (params) {
const { func } = params;
const tempProcedure = Object.assign({}, func);
tempProcedure.name = `Antares_${tempProcedure.name}_tmp`;
try {
await this.createFunction(tempProcedure);
await this.dropFunction({ func: tempProcedure.name });
await this.dropFunction({ func: func.oldName });
await this.createFunction(func);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE FUNCTION
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createFunction (func) {
const parameters = func.parameters.reduce((acc, curr) => {
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
const sql = `CREATE ${func.definer ? `DEFINER=${func.definer} ` : ''}FUNCTION \`${func.name}\`(${parameters}) RETURNS ${func.returns}${func.returnsLength ? `(${func.returnsLength})` : ''}
LANGUAGE SQL
${func.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${func.dataAccess}
SQL SECURITY ${func.security}
COMMENT '${func.comment}'
${func.sql}`;
return await this.raw(sql, { split: false });
}
/**
* SHOW CREATE EVENT
*
* @returns {Array.<Object>} view informations
* @memberof MySQLClient
*/
async getEventInformations ({ schema, scheduler }) {
const sql = `SHOW CREATE EVENT \`${schema}\`.\`${scheduler}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
const schedule = row['Create Event'];
const execution = schedule.includes('EVERY') ? 'EVERY' : 'ONCE';
const every = execution === 'EVERY' ? row['Create Event'].match(/(?<=EVERY )(\s*([^\s]+)){0,2}/gs)[0].replaceAll('\'', '').split(' ') : [];
const starts = execution === 'EVERY' && schedule.includes('STARTS') ? schedule.match(/(?<=STARTS ').*?(?='\s)/gs)[0] : '';
const ends = execution === 'EVERY' && schedule.includes('ENDS') ? schedule.match(/(?<=ENDS ').*?(?='\s)/gs)[0] : '';
const at = execution === 'ONCE' && schedule.includes('AT') ? schedule.match(/(?<=AT ').*?(?='\s)/gs)[0] : '';
return {
definer: row['Create Event'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Event'].match(/(?<=DO )(.*)/gs)[0],
name: row.Event,
comment: row['Create Event'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Event'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
state: row['Create Event'].includes('ENABLE') ? 'ENABLE' : row['Create Event'].includes('DISABLE ON SLAVE') ? 'DISABLE ON SLAVE' : 'DISABLE',
preserve: row['Create Event'].includes('ON COMPLETION PRESERVE'),
execution,
every,
starts,
ends,
at
};
})[0];
}
/**
* DROP EVENT
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropEvent (params) {
const sql = `DROP EVENT \`${params.scheduler}\``;
return await this.raw(sql);
}
/**
* ALTER EVENT
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterEvent (params) {
const { scheduler } = params;
if (scheduler.execution === 'EVERY' && scheduler.every[0].includes('-'))
scheduler.every[0] = `'${scheduler.every[0]}'`;
const sql = `ALTER ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.oldName}\`
ON SCHEDULE
${scheduler.execution === 'EVERY'
? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}`
: `AT '${scheduler.at}'`}
ON COMPLETION${!scheduler.preserve ? ' NOT' : ''} PRESERVE
${scheduler.name !== scheduler.oldName ? `RENAME TO \`${scheduler.name}\`` : ''}
${scheduler.state}
COMMENT '${scheduler.comment}'
DO ${scheduler.sql}`;
return await this.raw(sql, { split: false });
}
/**
* CREATE EVENT
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createEvent (scheduler) {
const sql = `CREATE ${scheduler.definer ? ` DEFINER=${scheduler.definer}` : ''} EVENT \`${scheduler.name}\`
ON SCHEDULE
${scheduler.execution === 'EVERY'
? `EVERY ${scheduler.every.join(' ')}${scheduler.starts ? ` STARTS '${scheduler.starts}'` : ''}${scheduler.ends ? ` ENDS '${scheduler.ends}'` : ''}`
: `AT '${scheduler.at}'`}
ON COMPLETION${!scheduler.preserve ? ' NOT' : ''} PRESERVE
${scheduler.state}
COMMENT '${scheduler.comment}'
DO ${scheduler.sql}`;
return await this.raw(sql, { split: false });
}
/**
* SHOW COLLATION
*
@@ -589,6 +965,54 @@ export class MySQLClient extends AntaresCore {
});
}
/**
* SHOW VARIABLES LIKE '%vers%'
*
* @returns {Array.<Object>} version parameters
* @memberof MySQLClient
*/
async getVersion () {
const sql = 'SHOW VARIABLES LIKE "%vers%"';
const { rows } = await this.raw(sql);
return rows.reduce((acc, curr) => {
switch (curr.Variable_name) {
case 'version':
acc.number = curr.Value.split('-')[0];
break;
case 'version_comment':
acc.name = curr.Value.replace('(GPL)', '');
break;
case 'version_compile_machine':
acc.arch = curr.Value;
break;
case 'version_compile_os':
acc.os = curr.Value;
break;
}
return acc;
}, {});
}
async getProcesses () {
const sql = 'SELECT `ID`, `USER`, `HOST`, `DB`, `COMMAND`, `TIME`, `STATE`, LEFT(`INFO`, 51200) AS `INFO` FROM `information_schema`.`PROCESSLIST`';
const { rows } = await this.raw(sql);
return rows.map(row => {
return {
id: row.ID,
user: row.USER,
host: row.HOST,
db: row.DB,
command: row.COMMAND,
time: row.TIME,
state: row.STATE,
info: row.INFO
};
});
}
/**
* CREATE TABLE
*
@@ -636,7 +1060,8 @@ export class MySQLClient extends AntaresCore {
// ADD FIELDS
additions.forEach(addition => {
const length = addition.numLength || addition.charLength || addition.datePrecision;
const typeInfo = this._getTypeInfo(addition.type);
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN \`${addition.name}\`
${addition.type.toUpperCase()}${length ? `(${length})` : ''}
@@ -673,7 +1098,8 @@ export class MySQLClient extends AntaresCore {
// CHANGE FIELDS
changes.forEach(change => {
const length = change.numLength || change.charLength || change.datePrecision;
const typeInfo = this._getTypeInfo(change.type);
const length = typeInfo.length ? change.numLength || change.charLength || change.datePrecision : false;
alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\`
${change.type.toUpperCase()}${length ? `(${length})` : ''}
@@ -795,18 +1221,11 @@ export class MySQLClient extends AntaresCore {
// INSERT
let insertRaw = '';
if (Object.keys(this._query.insert).length) {
const fieldsList = [];
const valueList = [];
const fields = this._query.insert;
if (this._query.insert.length) {
const fieldsList = Object.keys(this._query.insert[0]);
const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`);
for (const key in fields) {
if (fields[key] === null) continue;
fieldsList.push(key);
valueList.push(fields[key]);
}
insertRaw = `(${fieldsList.join(', ')}) VALUES (${valueList.join(', ')}) `;
insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `;
}
// GROUP BY
@@ -842,49 +1261,51 @@ export class MySQLClient extends AntaresCore {
const nestTables = args.nest ? '.' : false;
const resultsArr = [];
let paramsArr = [];
let selectedFields = [];
const queries = args.split ? sql.split(';') : [sql];
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
for (const query of queries) {
if (!query) continue;
let fieldsArr = [];
const timeStart = new Date();
let timeStop;
let keysArr = [];
const { rows, report, fields, keys } = await new Promise((resolve, reject) => {
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
this._connection.query({ sql: query, nestTables }, async (err, response, fields) => {
timeStop = new Date();
const queryResult = response;
if (err)
reject(err);
else {
const remappedFields = fields
let remappedFields = fields
? fields.map(field => {
if (!field || Array.isArray(field))
return false;
const type = this._getType(field);
return {
name: field.name,
name: field.orgName,
alias: field.name,
orgName: field.orgName,
schema: field.db,
schema: field.schema,
table: field.table,
tableAlias: field.table,
orgTable: field.orgTable,
type: 'VARCHAR'
type: type.name,
length: type.length
};
})
}).filter(Boolean)
: [];
if (args.details) {
let cachedTable;
if (remappedFields.length) {
selectedFields = remappedFields.map(field => {
return {
name: field.orgName || field.name,
table: field.orgTable || field.table
};
});
paramsArr = remappedFields.map(field => {
if (field.table) cachedTable = field.table;// Needed for some queries on information_schema
if (field.orgTable) cachedTable = field.orgTable;// Needed for some queries on information_schema
return {
table: field.orgTable || cachedTable,
schema: field.schema || 'INFORMATION_SCHEMA'
@@ -892,43 +1313,16 @@ export class MySQLClient extends AntaresCore {
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
for (const paramObj of paramsArr) {
try { // Table data
if (!paramObj.table || !paramObj.schema) continue;
try { // Column details
const response = await this.getTableColumns(paramObj);
let detailedFields = response.length
? selectedFields.map(selField => {
return response.find(field => field.name.toLowerCase() === selField.name.toLowerCase() && field.table === selField.table);
}).filter(el => !!el)
: [];
if (selectedFields.length) {
detailedFields = detailedFields.map(field => {
const aliasObj = remappedFields.find(resField => resField.orgName === field.name && resField.orgTable === field.table);
return {
...field,
alias: aliasObj.name || field.name,
tableAlias: aliasObj.table || field.table
};
});
}
if (!detailedFields.length) {
detailedFields = remappedFields.map(field => {
const isInFields = fieldsArr.some(f => field.name.toLowerCase() === f.name.toLowerCase() && field.table === f.table);
if (!isInFields) {
return {
...field,
alias: field.name,
tableAlias: field.table
};
}
else
return false;
}).filter(Boolean);
}
fieldsArr = fieldsArr ? [...fieldsArr, ...detailedFields] : detailedFields;
remappedFields = remappedFields.map(field => {
const detailedField = response.find(f => f.name === field.name);
if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema)
field = { ...detailedField, ...field };
return field;
});
}
catch (err) {
reject(err);
@@ -946,16 +1340,17 @@ export class MySQLClient extends AntaresCore {
}
resolve({
rows: Array.isArray(queryResult) ? queryResult : false,
duration: timeStop - timeStart,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false,
fields: fieldsArr.length ? fieldsArr : remappedFields,
fields: remappedFields,
keys: keysArr
});
}
});
});
resultsArr.push({ rows, report, fields, keys });
resultsArr.push({ rows, report, fields, keys, duration });
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { ipcRenderer } from 'electron';
import { ipcRenderer, remote } from 'electron';
export default {
name: 'App',
@@ -54,6 +54,45 @@ export default {
},
mounted () {
ipcRenderer.send('check-for-updates');
const Menu = remote.Menu;
const InputMenu = Menu.buildFromTemplate([
{
label: this.$t('word.cut'),
role: 'cut'
},
{
label: this.$t('word.copy'),
role: 'copy'
},
{
label: this.$t('word.paste'),
role: 'paste'
},
{
type: 'separator'
},
{
label: this.$t('message.selectAll'),
role: 'selectall'
}
]);
document.body.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
let node = e.target;
while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup(remote.getCurrentWindow());
break;
}
node = node.parentNode;
}
});
},
methods: {
...mapActions({

View File

@@ -24,7 +24,7 @@
<slot name="body" />
</div>
</div>
<div class="modal-footer">
<div v-if="!hideFooter" class="modal-footer">
<button
class="btn btn-primary mr-2"
@click.stop="confirmModal"
@@ -51,6 +51,10 @@ export default {
validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
default: 'small'
},
hideFooter: {
type: Boolean,
default: false
},
confirmText: String,
cancelText: String
},
@@ -74,6 +78,12 @@ export default {
else return '';
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
confirmModal () {
this.$emit('confirm');
@@ -82,6 +92,11 @@ export default {
hideModal () {
this.$emit('hide');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.hideModal();
}
}
};

View File

@@ -0,0 +1,22 @@
<template>
<div class="empty">
<div class="loading loading-lg" />
</div>
</template>
<script>
export default {
name: 'BaseLoader'
};
</script>
<style scoped>
.empty {
position: absolute;
display: flex;
height: 100%;
flex-direction: column;
left: 0;
justify-content: center;
right: 0;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div class="editor-wrapper">
<div
:id="`editor-${id}`"
class="editor"
:class="editorClass"
:style="{height: `${height}px`}"
/>
</div>
</template>
<script>
import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import { mapGetters } from 'vuex';
export default {
name: 'BaseTextEditor',
props: {
value: String,
mode: { type: String, default: 'text' },
editorClass: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
height: { type: Number, default: 200 }
},
data () {
return {
editor: null,
id: null
};
},
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
})
},
watch: {
mode () {
if (this.editor)
this.editor.session.setMode(`ace/mode/${this.mode}`);
},
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
autoComplete () {
if (this.editor) {
this.editor.setOptions({
enableLiveAutocompletion: this.autoComplete
});
}
},
lineWrap () {
if (this.editor) {
this.editor.setOptions({
wrap: this.lineWrap
});
}
}
},
created () {
this.id = this._uid;
},
mounted () {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value,
fontSize: '14px',
printMargin: false,
readOnly: this.readOnly
});
this.editor.setOptions({
enableBasicAutocompletion: false,
wrap: this.lineWrap,
enableSnippets: false,
enableLiveAutocompletion: false
});
this.editor.session.on('change', () => {
const content = this.editor.getValue();
this.$emit('update:value', content);
});
if (this.autoFocus) {
setTimeout(() => {
this.editor.focus();
this.editor.resize();
}, 20);
}
setTimeout(() => {
this.editor.resize();
}, 20);
}
};
</script>
<style lang="scss" scoped>
.editor-wrapper {
border-bottom: 1px solid #444;
.editor {
width: 100%;
}
}
.ace_.mdi {
display: inline-block;
width: 17px;
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
background-color: #c9561a99;
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover {
background-color: #c9571a33;
border: none;
}
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight {
color: #e0d00c;
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<label :for="`id_${id}`" class="file-uploader">
<span class="file-uploader-message">
<i class="mdi mdi-folder-open mr-1" />{{ message }}
</span>
<span class="text-ellipsis file-uploader-value">
{{ value | lastPart }}
</span>
<i
v-if="value.length"
class="file-uploader-reset mdi mdi-close"
@click.prevent="clear"
/>
<form :ref="`form_${id}`">
<input
:id="`id_${id}`"
class="file-uploader-input"
type="file"
@change="$emit('change', $event)"
>
</form>
</label>
</template>
<script>
export default {
name: 'BaseUploadInput',
filters: {
lastPart (string) {
if (!string) return '';
string = string.split(/[/\\]+/).pop();
if (string.length >= 19)
string = `...${string.slice(-19)}`;
return string;
}
},
props: {
message: {
default: 'Browse',
type: String
},
value: {
default: '',
type: String
}
},
data () {
return {
id: null
};
},
mounted () {
this.id = this._uid;
},
methods: {
clear () {
this.$emit('clear');
}
}
};
</script>
<style lang="scss" scoped>
.file-uploader {
border: 0.05rem solid $bg-color-light;
border-radius: 0.1rem;
height: 1.8rem;
line-height: 1.2rem;
display: flex;
cursor: pointer;
background-color: $bg-color-gray;
transition: background 0.2s, border 0.2s, box-shadow 0.2s, color 0.2s;
position: relative;
flex: 1 1 auto;
> span {
padding: 0.25rem 0.4rem;
}
.file-uploader-message {
display: flex;
border-right: 0.05rem solid $bg-color-light;
background-color: $bg-color;
}
.file-uploader-input {
display: none;
}
.file-uploader-value {
display: block;
width: 100%;
padding-right: 1rem;
}
.file-uploader-reset {
z-index: 1;
position: absolute;
right: 5px;
top: calc(50% - 8px);
}
}
:disabled {
.file-uploader {
cursor: not-allowed;
background-color: #151515;
opacity: 0.5;
}
}
</style>

View File

@@ -41,14 +41,16 @@ export default {
localScrollElement: null
};
},
watch: {
scrollElement () {
this.setScrollElement();
}
},
mounted () {
this._checkScrollPosition = this.checkScrollPosition.bind(this);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this._checkScrollPosition);
this.setScrollElement();
},
beforeDestroy () {
this.localScrollElement.removeEventListener('scroll', this._checkScrollPosition);
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
},
methods: {
checkScrollPosition (e) {
@@ -58,7 +60,7 @@ export default {
this.updateWindow(e);
}, 200);
},
updateWindow (e) {
updateWindow () {
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
const totalScrollHeight = this.items.length * this.itemHeight;
const offset = 50;
@@ -74,6 +76,14 @@ export default {
this.topHeight = firstCutIndex * this.itemHeight;
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
},
setScrollElement () {
if (this.localScrollElement)
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this.checkScrollPosition);
}
}
};

View File

@@ -0,0 +1,235 @@
<template>
<fieldset class="input-group mb-0">
<select
v-model="selectedGroup"
class="form-select"
:disabled="!isChecked"
style="flex-grow: 0;"
@change="onChange"
>
<option value="manual">
{{ $t('message.manualValue') }}
</option>
<option
v-for="group in fakerGroups"
:key="group.name"
:value="group.name"
>
{{ $t(`faker.${group.name}`) }}
</option>
</select>
<select
v-if="selectedGroup !== 'manual'"
v-model="selectedMethod"
class="form-select"
:disabled="!isChecked"
@change="onChange"
>
<option
v-for="method in fakerMethods"
:key="method.name"
:value="method.name"
>
{{ $t(`faker.${method.name}`) }}
</option>
</select>
<ForeignKeySelect
v-else-if="foreignKeys.includes(field.name)"
ref="formInput"
class="form-select"
:value.sync="selectedValue"
:key-usage="getKeyUsage(field.name)"
:disabled="!isChecked"
/>
<input
v-else-if="inputProps().mask"
ref="formInput"
v-model="selectedValue"
v-mask="inputProps().mask"
class="form-input"
:type="inputProps().type"
:disabled="!isChecked"
>
<BaseUploadInput
v-else-if="inputProps().type === 'file'"
:value="selectedValue"
:message="$t('word.browse')"
@clear="clearValue"
@change="filesChange($event)"
/>
<input
v-else-if="inputProps().type === 'number'"
ref="formInput"
v-model="selectedValue"
class="form-input"
step="any"
:type="inputProps().type"
:disabled="!isChecked"
>
<input
v-else
ref="formInput"
v-model="selectedValue"
class="form-input"
:type="inputProps().type"
:disabled="!isChecked"
>
<template v-if="methodData && 'params' in methodData" class="columns">
<input
v-for="(option, key) in methodData.params"
:key="key"
v-model="methodParams[option]"
class="form-input column"
:type="inputProps().type"
:disabled="!isChecked"
:placeholder="option"
>
</template>
<slot />
</fieldset>
</template>
<script>
import { VueMaskDirective } from 'v-mask';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import BaseUploadInput from '@/components/BaseUploadInput';
import ForeignKeySelect from '@/components/ForeignKeySelect';
import FakerMethods from 'common/FakerMethods';
export default {
name: 'FakerSelect',
components: {
ForeignKeySelect,
BaseUploadInput
},
directives: {
mask: VueMaskDirective
},
props: {
type: String,
field: Object,
isChecked: Boolean,
foreignKeys: Array,
keyUsage: Array,
fieldLength: Number,
fieldObj: Object
},
data () {
return {
localType: null,
selectedGroup: 'manual',
selectedMethod: '',
selectedValue: '',
debounceTimeout: null,
methodParams: {}
};
},
computed: {
fakerGroups () {
if ([...TEXT, ...LONG_TEXT].includes(this.type))
this.localType = 'string';
else if (NUMBER.includes(this.type))
this.localType = 'number';
else if (FLOAT.includes(this.type))
this.localType = 'float';
else if ([...DATE, ...DATETIME].includes(this.type))
this.localType = 'datetime';
else if (TIME.includes(this.type))
this.localType = 'time';
else
this.localType = 'none';
return FakerMethods.getGroupsByType(this.localType);
},
fakerMethods () {
return FakerMethods.getMethods({ type: this.localType, group: this.selectedGroup });
},
methodData () {
return this.fakerMethods.find(method => method.name === this.selectedMethod);
}
},
watch: {
fieldObj () {
if (this.fieldObj)
this.selectedValue = this.fieldObj.value;
},
selectedGroup () {
if (this.fakerMethods.length)
this.selectedMethod = this.fakerMethods[0].name;
else
this.selectedMethod = '';
},
selectedMethod () {
this.onChange();
},
selectedValue () {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = null;
this.debounceTimeout = setTimeout(() => {
this.onChange();
}, 200);
}
},
methods: {
inputProps () {
if ([...TEXT, ...LONG_TEXT].includes(this.type))
return { type: 'text', mask: false };
if ([...NUMBER, ...FLOAT].includes(this.type))
return { type: 'number', mask: false };
if (TIME.includes(this.type)) {
let timeMask = '##:##:##';
const precision = this.fieldLength;
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: timeMask };
}
if (DATE.includes(this.type))
return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(this.type)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.fieldLength;
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
if (BLOB.includes(this.type))
return { type: 'file', mask: false };
if (BIT.includes(this.type))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.field === keyName);
},
filesChange (event) {
const { files } = event.target;
if (!files.length) return;
this.selectedValue = files[0].path;
},
clearValue () {
this.selectedValue = '';
},
onChange () {
this.$emit('update:value', {
group: this.selectedGroup,
method: this.selectedMethod,
params: this.methodParams,
value: this.selectedValue,
length: this.fieldLength
});
}
}
};
</script>

View File

@@ -11,11 +11,11 @@
</option>
<option
v-for="row in foreignList"
:key="row.foreignColumn"
:value="row.foreignColumn"
:selected="row.foreignColumn === value"
:key="row.foreign_column"
:value="row.foreign_column"
:selected="row.foreign_column === value"
>
{{ row.foreignColumn }} {{ 'foreignDescription' in row ? ` - ${row.foreignDescription}` : '' | cutText }}
{{ row.foreign_column }} {{ 'foreign_description' in row ? ` - ${row.foreign_description}` : '' | cutText }}
</option>
</select>
</template>
@@ -50,11 +50,12 @@ export default {
selectedWorkspace: 'workspaces/getSelected'
}),
isValidDefault () {
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString());
if (!this.foreignList.length) return true;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString());
}
},
async created () {
let firstTextField;
let foreignDesc;
const params = {
uid: this.selectedWorkspace,
schema: this.keyUsage.refSchema,
@@ -63,8 +64,10 @@ export default {
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success')
firstTextField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type)).name || false;
if (status === 'success') {
const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type));
foreignDesc = textField ? textField.name : false;
}
else
this.addNotification({ status: 'error', message: response });
}
@@ -76,7 +79,7 @@ export default {
const { status, response } = await Tables.getForeignList({
...params,
column: this.keyUsage.refField,
description: firstTextField
description: foreignDesc
});
if (status === 'success')

View File

@@ -15,7 +15,7 @@
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.user') }}:</label>
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-9">
<input
@@ -28,7 +28,7 @@
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.password') }}:</label>
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-9">
<input

View File

@@ -0,0 +1,107 @@
<template>
<ConfirmModal
:confirm-text="$t('word.run')"
:cancel-text="$t('word.cancel')"
size="400"
@confirm="runRoutine"
@hide="closeModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-play mr-1" /> {{ $t('word.parameters') }}: {{ localRoutine.name }}
</div>
</template>
<div slot="body">
<div class="content">
<form class="form-horizontal">
<div
v-for="(parameter, i) in localRoutine.parameters"
:key="parameter._id"
class="form-group"
>
<div class="col-3">
<label class="form-label">{{ parameter.name }}</label>
</div>
<div class="col-9">
<div class="input-group">
<input
:ref="i === 0 ? 'firstInput' : ''"
v-model="values[parameter.name]"
class="form-input"
type="text"
>
<span class="input-group-addon field-type" :class="typeClass(parameter.type)">
{{ parameter.type }} {{ parameter.length | wrapNumber }}
</span>
</div>
</div>
</div>
</form>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalAskParameters',
components: {
ConfirmModal
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
localRoutine: Object
},
data () {
return {
values: {}
};
},
created () {
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput[0].focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr) => {
const value = isNaN(this.values[curr]) ? `"${this.values[curr]}"` : this.values[curr];
acc.push(value);
return acc;
}, []);
this.$emit('confirm', valArr);
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style scoped>
.field-type {
font-size: 0.6rem;
}
</style>

View File

@@ -10,151 +10,267 @@
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}:</label>
<div class="modal-body p-0">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="c-hand">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="c-hand">{{ $t('word.ssl') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="localConnection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
</div>
<div class="col-8 col-sm-12">
<select v-model="localConnection.client" class="form-select">
<option value="mysql">
MySQL
</option>
<option value="maria">
MariaDB
</option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.host"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.user"
class="form-input"
type="text"
:disabled="localConnection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.password"
class="form-input"
type="password"
:disabled="localConnection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12" />
<div class="col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsl') }}
</label>
</div>
<div class="col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="localConnection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="localConnection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}:</label>
</div>
<div class="col-8 col-sm-12">
<select v-model="localConnection.client" class="form-select">
<option value="mysql">
MySQL
</option>
<option value="maria">
MariaDB
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="pg">
PostgreSQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.host"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.user"
class="form-input"
type="text"
:disabled="localConnection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.password"
class="form-input"
type="password"
:disabled="localConnection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12" />
<div class="col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }}
</label>
</div>
</div>
</fieldset>
</form>
<fieldset class="m-0" :disabled="isTesting || !localConnection.ssl">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.key"
:message="$t('word.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.cert"
:message="$t('word.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.ca"
:message="$t('word.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="localConnection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div class="modal-footer text-light">
<button
class="btn btn-gray mr-2"
:class="{'loading': isTesting}"
@click="startTest"
>
{{ $t('message.testConnection') }}
</button>
<button class="btn btn-primary mr-2" @click="saveEditConnection">
{{ $t('word.save') }}
</button>
<button class="btn btn-link" @click="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
<div class="modal-footer text-light">
<button
class="btn btn-gray mr-2"
:class="{'loading': isTesting}"
@click="startTest"
>
{{ $t('message.testConnection') }}
</button>
<button class="btn btn-primary mr-2" @click="saveEditConnection">
{{ $t('word.save') }}
</button>
<button class="btn btn-link" @click="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast';
import BaseUploadInput from '@/components/BaseUploadInput';
export default {
name: 'ModalEditConnection',
components: {
ModalAskCredentials,
BaseToast
BaseToast,
BaseUploadInput
},
props: {
connection: Object
@@ -167,9 +283,15 @@ export default {
},
isTesting: false,
isAsking: false,
localConnection: null
localConnection: null,
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () {
this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey);
@@ -240,6 +362,21 @@ export default {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.localConnection.ssl = !this.localConnection.ssl;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.localConnection[name] = files[0].path;
},
pathClear (name) {
this.localConnection[name] = '';
}
}
};
@@ -247,6 +384,8 @@ export default {
<style scoped>
.modal-container {
position: absolute;
max-width: 450px;
top: 17.5vh;
}
</style>

View File

@@ -5,7 +5,7 @@
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editDatabase') }}
<i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editSchema') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -15,7 +15,7 @@
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}:</label>
<label class="form-label">{{ $t('word.name') }}</label>
</div>
<div class="col-9">
<input
@@ -23,14 +23,14 @@
class="form-input"
type="text"
required
:placeholder="$t('message.databaseName')"
:placeholder="$t('message.schemaName')"
readonly
>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}:</label>
<label class="form-label">{{ $t('word.collation') }}</label>
</div>
<div class="col-9">
<select
@@ -53,7 +53,7 @@
</div>
</div>
<div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="updateDatabase">
<button class="btn btn-primary mr-2" @click.stop="updateSchema">
{{ $t('word.update') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
@@ -66,10 +66,10 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database';
import Schema from '@/ipc-api/Schema';
export default {
name: 'ModalEditDatabase',
name: 'ModalEditSchema',
props: {
selectedDatabase: String
},
@@ -98,7 +98,7 @@ export default {
async created () {
let actualCollation;
try {
const { status, response } = await Database.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase });
const { status, response } = await Schema.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase });
if (status === 'success')
actualCollation = response;
@@ -130,10 +130,10 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async updateDatabase () {
async updateSchema () {
if (this.database.collation !== this.database.prevCollation) {
try {
const { status, response } = await Database.updateDatabase({
const { status, response } = await Schema.updateSchema({
uid: this.selectedWorkspace,
...this.database
});

View File

@@ -0,0 +1,392 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> {{ $t('message.tableFiller') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset :disabled="isInserting">
<div
v-for="field in fields"
:key="field.name"
class="form-group"
>
<div class="col-3 col-sm-12">
<label class="form-label" :title="field.name">{{ field.name }}</label>
</div>
<div class="column columns col-sm-12">
<FakerSelect
:type="field.type"
class="column columns pr-0"
:is-checked="!fieldsToExclude.includes(field.name)"
:foreign-keys="foreignKeys"
:key-usage="keyUsage"
:field="field"
:field-length="fieldLength(field)"
:field-obj="localRow[field.name]"
:value.sync="localRow[field.name]"
>
<span class="input-group-addon field-type" :class="typeClass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!field.autoIncrement"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
</FakerSelect>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="modal-footer text-light columns">
<div class="column d-flex" :class="hasFakes ? 'col-4' : 'col-2'">
<div class="input-group tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<input
v-model="nInserts"
type="number"
class="form-input"
min="1"
:disabled="isInserting"
>
<span class="input-group-addon">
<i class="mdi mdi-24px mdi-repeat" />
</span>
</div>
<div
v-if="hasFakes"
class="tooltip tooltip-right ml-2"
:data-tooltip="$t('message.fakeDataLanguage')"
>
<select v-model="fakerLocale" class="form-select">
<option value="ar">
Arabic
</option><option value="az">
Azerbaijani
</option><option value="zh_CN">
Chinese
</option><option value="zh_TW">
Chinese (Taiwan)
</option><option value="cz">
Czech
</option><option value="nl">
Dutch
</option><option value="nl_BE">
Dutch (Belgium)
</option><option value="en">
English
</option><option value="en_AU_ocker">
English (Australia Ocker)
</option><option value="en_AU">
English (Australia)
</option><option value="en_BORK">
English (Bork)
</option><option value="en_CA">
English (Canada)
</option><option value="en_GB">
English (Great Britain)
</option><option value="en_IND">
English (India)
</option><option value="en_IE">
English (Ireland)
</option><option value="en_ZA">
English (South Africa)
</option><option value="en_US">
English (United States)
</option><option value="fa">
Farsi
</option><option value="fi">
Finnish
</option><option value="fr">
French
</option><option value="fr_CA">
French (Canada)
</option><option value="fr_CH">
French (Switzerland)
</option><option value="ge">
Georgian
</option><option value="de">
German
</option><option value="de_AT">
German (Austria)
</option><option value="de_CH">
German (Switzerland)
</option><option value="hr">
Hrvatski
</option><option value="id_ID">
Indonesia
</option><option value="it">
Italian
</option><option value="ja">
Japanese
</option><option value="ko">
Korean
</option><option value="nep">
Nepalese
</option><option value="nb_NO">
Norwegian
</option><option value="pl">
Polish
</option><option value="pt_BR">
Portuguese (Brazil)
</option><option value="pt_PT">
Portuguese (Portugal)
</option><option value="ro">
Romanian
</option><option value="ru">
Russian
</option><option value="sk">
Slovakian
</option><option value="es">
Spanish
</option><option value="es_MX">
Spanish (Mexico)
</option><option value="sv">
Swedish
</option><option value="tr">
Turkish
</option><option value="uk">
Ukrainian
</option><option value="vi">
Vietnamese
</option>
</select>
</div>
</div>
<div class="column col-auto">
<button
class="btn btn-primary mr-2"
:class="{'loading': isInserting}"
@click.stop="insertRows"
>
{{ $t('word.insert') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect';
export default {
name: 'ModalFakerRows',
components: {
FakerSelect
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
tabUid: [String, Number],
fields: Array,
keyUsage: Array
},
data () {
return {
localRow: {},
fieldsToExclude: [],
nInserts: 1,
isInserting: false,
fakerLocale: 'en'
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getWorkspaceTab: 'workspaces/getWorkspaceTab'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
foreignKeys () {
return this.keyUsage.map(key => key.field);
},
hasFakes () {
return Object.keys(this.localRow).some(field => 'group' in this.localRow[field] && this.localRow[field].group !== 'manual');
}
},
watch: {
nInserts (val) {
if (!val || val < 1)
this.nInserts = 1;
else if (val > 1000)
this.nInserts = 1000;
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
const rowObj = {};
for (const field of this.fields) {
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = +field.default;
if ([...TEXT, ...LONG_TEXT].includes(field.type))
fieldDefault = field.default ? field.default.substring(1, field.default.length - 1) : '';
if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
if (DATETIME.includes(field.type)) {
if (field.default && field.default.toLowerCase().includes('current_timestamp')) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
}
}
rowObj[field.name] = { value: fieldDefault };
if (field.autoIncrement)// Disable by default auto increment fields
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
}
this.localRow = { ...rowObj };
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () {
this.isInserting = true;
const rowToInsert = this.localRow;
Object.keys(rowToInsert).forEach(key => {
if (this.fieldsToExclude.includes(key))
delete rowToInsert[key];
if (typeof rowToInsert[key] === 'undefined')
delete rowToInsert[key];
});
const fieldTypes = {};
this.fields.forEach(field => {
fieldTypes[field.name] = field.type;
});
try {
const { status, response } = await Tables.insertTableFakeRows({
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table,
row: rowToInsert,
repeat: this.nInserts,
fields: fieldTypes,
locale: this.fakerLocale
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isInserting = false;
},
closeModal () {
this.$emit('hide');
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return field.charLength;
return field.length;
},
toggleFields (event, field) {
if (event.target.checked)
this.fieldsToExclude = this.fieldsToExclude.filter(f => f !== field.name);
else
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
},
filesChange (event, field) {
const { files } = event.target;
if (!files.length) return;
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.field === keyName);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style scoped>
.modal-container {
max-width: 800px;
}
.form-label {
overflow: hidden;
white-space: normal;
text-overflow: ellipsis;
}
.input-group-addon {
display: flex;
align-items: center;
}
.modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.field-type {
font-size: 0.6rem;
}
</style>

View File

@@ -10,119 +10,232 @@
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}:</label>
<div class="modal-body p-0">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="c-hand">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="c-hand">{{ $t('word.ssl') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
</div>
<div class="col-8 col-sm-12">
<select
v-model="connection.client"
class="form-select"
@change="setDefaults"
>
<option value="mysql">
MySQL
</option>
<option value="maria">
MariaDB
</option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12" />
<div class="col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsl') }}
</label>
</div>
<div class="col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="connection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}:</label>
</div>
<div class="col-8 col-sm-12">
<select
v-model="connection.client"
class="form-select"
@change="setDefaults"
>
<option value="mysql">
MySQL
</option>
<option value="maria">
MariaDB
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="pg">
PostgreSQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12" />
<div class="col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }}
</label>
</div>
</div>
</fieldset>
</form>
<fieldset class="m-0" :disabled="isTesting || !connection.ssl">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="connection.key"
:message="$t('word.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="connection.cert"
:message="$t('word.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="connection.ca"
:message="$t('word.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div class="modal-footer text-light">
<button
@@ -150,16 +263,19 @@
<script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen';
import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast';
import BaseUploadInput from '@/components/BaseUploadInput';
export default {
name: 'ModalNewConnection',
components: {
ModalAskCredentials,
BaseToast
BaseToast,
BaseUploadInput
},
data () {
return {
@@ -167,21 +283,35 @@ export default {
name: '',
client: 'mysql',
host: '127.0.0.1',
port: '3306',
user: 'root',
database: null,
port: null,
user: null,
password: '',
ask: false,
uid: uidGen('C')
uid: uidGen('C'),
ssl: false,
cert: '',
key: '',
ca: '',
ciphers: ''
},
toast: {
status: '',
message: ''
},
isTesting: false,
isAsking: false
isAsking: false,
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () {
this.setDefaults();
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
@@ -197,20 +327,9 @@ export default {
addConnection: 'connections/addConnection'
}),
setDefaults () {
switch (this.connection.client) {
case 'mysql':
this.connection.port = '3306';
break;
case 'mssql':
this.connection.port = '1433';
break;
case 'pg':
this.connection.port = '5432';
break;
case 'oracledb':
this.connection.port = '1521';
break;
}
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
},
async startTest () {
this.isTesting = true;
@@ -265,6 +384,21 @@ export default {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.connection.ssl = !this.connection.ssl;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.connection[name] = files[0].path;
},
pathClear (name) {
this.connection[name] = '';
}
}
};
@@ -272,6 +406,8 @@ export default {
<style scoped>
.modal-container {
position: absolute;
max-width: 450px;
top: 17.5vh;
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewFunction"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewRoutine') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localFunction.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localFunction.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.returns') }}
</label>
<div class="column">
<div class="input-group">
<select
v-model="localFunction.returns"
class="form-select text-uppercase"
style="width: 0;"
>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="localFunction.returns === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
<input
v-model="localFunction.returnsLength"
class="form-input"
type="number"
min="0"
>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="localFunction.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="localFunction.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewFunction',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localFunction: {
definer: '',
sql: 'BEGIN\r\n RETURN NULL;\r\nEND',
parameters: [],
name: '',
comment: '',
returns: 'INT',
returnsLength: 10,
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
}
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewFunction () {
this.$emit('open-create-function-editor', this.localFunction);
}
}
};
</script>

View File

@@ -0,0 +1,109 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewTrigger"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewScheduler') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localScheduler.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localScheduler.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localScheduler.comment"
class="form-input"
type="text"
>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewScheduler',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localScheduler: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
comment: '',
execution: 'EVERY',
every: ['1', 'DAY'],
preserve: true,
state: 'DISABLE'
}
};
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewTrigger () {
this.$emit('open-create-scheduler-editor', this.localScheduler);
}
}
};
</script>

View File

@@ -5,7 +5,7 @@
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewDatabase') }}
<i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewSchema') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -15,7 +15,7 @@
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}:</label>
<label class="form-label">{{ $t('word.name') }}</label>
</div>
<div class="col-9">
<input
@@ -24,13 +24,13 @@
class="form-input"
type="text"
required
:placeholder="$t('message.databaseName')"
:placeholder="$t('message.schemaName')"
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.collations" class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}:</label>
<label class="form-label">{{ $t('word.collation') }}</label>
</div>
<div class="col-9">
<select v-model="database.collation" class="form-select">
@@ -49,7 +49,11 @@
</div>
</div>
<div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="createDatabase">
<button
class="btn btn-primary mr-2"
:class="{'loading': isLoading}"
@click.stop="createSchema"
>
{{ $t('word.add') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
@@ -62,12 +66,13 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database';
import Schema from '@/ipc-api/Schema';
export default {
name: 'ModalNewDatabase',
name: 'ModalNewSchema',
data () {
return {
isLoading: false,
database: {
name: '',
collation: ''
@@ -83,8 +88,11 @@ export default {
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
},
customizations () {
return this.getWorkspace(this.selectedWorkspace).customizations;
},
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server') ? this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value : '';
}
},
created () {
@@ -101,9 +109,10 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async createDatabase () {
async createSchema () {
this.isLoading = true;
try {
const { status, response } = await Database.createDatabase({
const { status, response } = await Schema.createSchema({
uid: this.selectedWorkspace,
...this.database
});
@@ -118,6 +127,7 @@ export default {
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isLoading = false;
},
closeModal () {
this.$emit('close');

View File

@@ -50,6 +50,16 @@
:tabindex="key+1"
@change="filesChange($event,field.name)"
>
<input
v-else-if="inputProps(field).type === 'number'"
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
step="any"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else
ref="formInput"
@@ -59,7 +69,7 @@
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<span class="input-group-addon" :class="`type-${field.type.toLowerCase()}`">
<span class="input-group-addon" :class="typeCLass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
@@ -107,8 +117,8 @@
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect';
@@ -119,7 +129,7 @@ export default {
ForeignKeySelect
},
directives: {
mask
mask: VueMaskDirective
},
filters: {
wrapNumber (num) {
@@ -157,6 +167,8 @@ export default {
nInserts (val) {
if (!val || val < 1)
this.nInserts = 1;
else if (val > 1000)
this.nInserts = 1000;
}
},
created () {
@@ -170,7 +182,7 @@ export default {
if (field.default === 'NULL') fieldDefault = null;
else {
if (NUMBER.includes(field.type))
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = +field.default;
if ([...TEXT, ...LONG_TEXT].includes(field.type))
@@ -210,6 +222,11 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification'
}),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () {
this.isInserting = true;
const rowToInsert = this.localRow;
@@ -253,13 +270,14 @@ export default {
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numLength || field.datePrecision || field.charLength || 0;
else if (TEXT.includes(field.type)) return field.charLength;
return field.length;
},
inputProps (field) {
if ([...TEXT, ...LONG_TEXT].includes(field.type))
return { type: 'text', mask: false };
if (NUMBER.includes(field.type))
if ([...NUMBER, ...FLOAT].includes(field.type))
return { type: 'number', mask: false };
if (TIME.includes(field.type)) {

View File

@@ -0,0 +1,310 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-memory mr-1" /> {{ $t('message.processesList') }}: {{ connectionName }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="processes-toolbar py-2 px-4">
<div class="dropdown">
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
:class="{'loading':isQuering}"
title="F5"
@click="getProcessesList"
>
<span>{{ $t('word.refresh') }}</span>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh ml-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h ml-1" />
</button>
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
<i class="mdi mdi-24px mdi-menu-down" />
</div>
<div class="menu px-3">
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
<input
v-model="autorefreshTimer"
class="slider no-border"
type="range"
min="0"
max="15"
step="0.5"
@change="setRefreshInterval"
>
</div>
</div>
</div>
<div class="workspace-query-info">
<div v-if="sortedResults.length">
{{ $t('word.processes') }}: <b>{{ sortedResults.length.toLocaleString() }}</b>
</div>
</div>
</div>
<div class="modal-body py-0 workspace-query-results">
<div
ref="tableWrapper"
class="vscroll"
:style="{'height': resultsSize+'px'}"
>
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr">
<div
v-for="(field, index) in fields"
:key="index"
class="th c-hand"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field)">
<span>{{ field.toUpperCase() }}</span>
<i
v-if="currentSort === field"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
</div>
</div>
</div>
</div>
</div>
<BaseVirtualScroll
ref="resultTable"
:items="sortedResults"
:item-height="22"
class="tbody"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<ProcessesListRow
v-for="row in items"
:key="row._id"
class="process-row"
:row="row"
@contextmenu="contextMenu"
@stop-refresh="stopRefresh"
/>
</template>
</BaseVirtualScroll>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import Schema from '@/ipc-api/Schema';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ProcessesListRow from '@/components/ProcessesListRow';
export default {
name: 'ModalProcessesList',
components: {
BaseVirtualScroll,
ProcessesListRow
},
props: {
connection: Object
},
data () {
return {
resultsSize: 1000,
isQuering: false,
autorefreshTimer: 0,
refreshInterval: null,
results: [],
fields: [],
currentSort: '',
currentSortDir: 'asc',
scrollElement: null
};
},
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName'
}),
connectionName () {
return this.getConnectionName(this.connection.uid);
},
sortedResults () {
if (this.currentSort) {
return [...this.results].sort((a, b) => {
let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
if (this.currentSortDir === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier;
return 0;
});
}
else
return this.results;
}
},
created () {
window.addEventListener('keydown', this.onKey, { capture: true });
},
updated () {
if (this.$refs.table)
this.refreshScroller();
if (this.$refs.tableWrapper)
this.scrollElement = this.$refs.tableWrapper;
},
mounted () {
this.getProcessesList();
window.addEventListener('resize', this.resizeResults);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async getProcessesList () {
this.isQuering = true;
// if table changes clear cached values
if (this.lastTable !== this.table)
this.results = [];
try { // Table data
const { status, response } = await Schema.getProcesses(this.connection.uid);
if (status === 'success') {
this.results = response;
this.fields = response.length ? Object.keys(response[0]) : [];
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
setRefreshInterval () {
this.clearRefresh();
if (+this.autorefreshTimer) {
this.refreshInterval = setInterval(() => {
if (!this.isQuering)
this.getProcessesList();
}, this.autorefreshTimer * 1000);
}
},
clearRefresh () {
if (this.refreshInterval)
clearInterval(this.refreshInterval);
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el) {
const size = el.offsetHeight;
this.resultsSize = size;
}
this.$refs.resultTable.updateWindow();
}
},
refreshScroller () {
this.resizeResults();
},
sort (field) {
if (field === this.currentSort) {
if (this.currentSortDir === 'asc')
this.currentSortDir = 'desc';
else
this.resetSort();
}
else {
this.currentSortDir = 'asc';
this.currentSort = field;
}
},
resetSort () {
this.currentSort = '';
this.currentSortDir = 'asc';
},
stopRefresh () {
this.autorefreshTimer = 0;
this.clearRefresh();
},
contextMenu () {},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
if (e.key === 'F5')
this.getProcessesList();
}
}
};
</script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.column-resizable {
&:hover,
&:active {
resize: horizontal;
overflow: hidden;
}
}
.table-column-title {
display: flex;
align-items: center;
}
.sort-icon {
font-size: 0.7rem;
line-height: 1;
margin-left: 0.2rem;
}
.result-tabs {
background: transparent !important;
margin: 0;
}
.modal {
align-items: flex-start;
.modal-container {
max-width: 75vw;
margin-top: 10vh;
.modal-body {
height: 80vh;
}
}
}
.processes-toolbar {
display: flex;
justify-content: space-between;
}
</style>

View File

@@ -56,7 +56,7 @@
<div class="col-6 col-sm-12">
<label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }}:
{{ $t('word.language') }}
</label>
</div>
<div class="col-6 col-sm-12">
@@ -78,7 +78,7 @@
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
{{ $t('message.notificationsTimeout') }}:
{{ $t('message.notificationsTimeout') }}
</label>
</div>
<div class="col-6 col-sm-12">
@@ -103,7 +103,7 @@
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
{{ $t('word.autoCompletion') }}:
{{ $t('word.autoCompletion') }}
</label>
</div>
<div class="col-6 col-sm-12">
@@ -118,7 +118,7 @@
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
{{ $t('message.wrapLongLines') }}:
{{ $t('message.wrapLongLines') }}
</label>
</div>
<div class="col-6 col-sm-12">
@@ -205,9 +205,9 @@
<img :src="require('@/images/logo.svg').default" width="128">
<h4>{{ appName }}</h4>
<p>
{{ $t('word.version') }}: {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a><br>
<small>{{ $t('word.author') }}: <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
{{ $t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a> | <a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares/blob/master/CHANGELOG.md')">CHANGELOG</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
<small>{{ $t('message.madeWithJS') }}</small>
</p>
</div>

View File

@@ -36,11 +36,17 @@
{{ $t('message.restartToInstall') }}
</button>
</div>
<div class="form-group mt-4">
<label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease">
<input type="checkbox" :checked="allowPrerelease">
<i class="form-icon" /> {{ $t('message.includeBetaUpdates') }}
</label>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import { ipcRenderer } from 'electron';
export default {
@@ -48,7 +54,8 @@ export default {
computed: {
...mapGetters({
updateStatus: 'application/getUpdateStatus',
downloadPercentage: 'application/getDownloadProgress'
downloadPercentage: 'application/getDownloadProgress',
allowPrerelease: 'settings/getAllowPrerelease'
}),
updateMessage () {
switch (this.updateStatus) {
@@ -70,11 +77,17 @@ export default {
}
},
methods: {
...mapActions({
changeAllowPrerelease: 'settings/changeAllowPrerelease'
}),
checkForUpdates () {
ipcRenderer.send('check-for-updates');
},
restartToUpdate () {
ipcRenderer.send('restart-to-update');
},
toggleAllowPrerelease () {
this.changeAllowPrerelease(!this.allowPrerelease);
}
}
};

View File

@@ -0,0 +1,155 @@
<template>
<div class="tr" @click="selectRow($event, row._id)">
<div
v-for="(col, cKey) in row"
v-show="cKey !== '_id'"
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
</template>
</div>
<ConfirmModal
v-if="isInfoModal"
:confirm-text="$t('word.update')"
:cancel-text="$t('word.close')"
size="medium"
:hide-footer="true"
@hide="hideInfoModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
</div>
</template>
<div :slot="'body'">
<div>
<div>
<TextEditor
:value="row.info || ''"
editor-class="textarea-editor"
:mode="editorMode"
:read-only="true"
/>
</div>
</div>
</div>
</ConfirmModal>
</div>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
export default {
name: 'ProcessesListRow',
components: {
ConfirmModal,
TextEditor
},
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
}
},
props: {
row: Object
},
data () {
return {
isInlineEditor: {},
isInfoModal: false,
editorMode: 'sql'
};
},
computed: {},
watch: {
fields () {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
},
methods: {
isNull (value) {
return value === null ? ' is-null' : '';
},
selectRow (event, row) {
this.$emit('select-row', event, row);
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
},
hideInfoModal () {
this.isInfoModal = false;
},
dblClick (col) {
if (col !== 'info') return;
this.$emit('stop-refresh');
this.isInfoModal = true;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
}
}
};
</script>
<style lang="scss">
.editable-field {
margin: 0;
border: none;
line-height: 1;
width: 100%;
position: absolute;
left: 0;
right: 0;
}
.cell-content {
display: block;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.textarea-editor {
height: 50vh !important;
}
.editor-field-info {
margin-top: 0.4rem;
display: flex;
justify-content: space-between;
white-space: normal;
}
.editor-buttons {
display: flex;
justify-content: space-evenly;
.btn {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="editor-wrapper">
<div
ref="editor"
:id="`editor-${id}`"
class="editor"
:style="{height: `${height}px`}"
/>
@@ -12,7 +12,7 @@
import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools';
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
export default {
@@ -20,6 +20,7 @@ export default {
props: {
value: String,
workspace: Object,
isSelected: Boolean,
schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
@@ -29,14 +30,17 @@ export default {
return {
editor: null,
fields: [],
baseCompleter: []
customCompleter: [],
id: null,
lastSchema: null
};
},
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
lineWrap: 'settings/getLineWrap',
baseCompleter: 'application/getBaseCompleter'
}),
tables () {
return this.workspace
@@ -47,13 +51,68 @@ export default {
}, []).map(table => {
return {
name: table.name,
comment: table.comment,
type: table.type,
fields: []
};
})
: [];
},
triggers () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.triggers);
return acc;
}, []).map(trigger => {
return {
name: trigger.name,
type: 'trigger'
};
})
: [];
},
procedures () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.procedures);
return acc;
}, []).map(procedure => {
return {
name: `${procedure.name}()`,
type: 'routine'
};
})
: [];
},
functions () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.functions);
return acc;
}, []).map(func => {
return {
name: `${func.name}()`,
type: 'function'
};
})
: [];
},
schedulers () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.schedulers);
return acc;
}, []).map(scheduler => {
return {
name: scheduler.name,
type: 'scheduler'
};
})
: [];
},
mode () {
switch (this.workspace.client) {
case 'mysql':
@@ -108,10 +167,24 @@ export default {
wrap: this.lineWrap
});
}
},
isSelected () {
if (this.isSelected)
this.lastSchema = this.schema;
},
lastSchema () {
if (this.editor) {
this.editor.completers = this.baseCompleter.map(el => Object.assign({}, el));
this.setCustomCompleter();
}
}
},
created () {
this.id = this._uid;
this.lastSchema = this.schema;
},
mounted () {
this.editor = ace.edit(this.$refs.editor, {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value,
@@ -127,21 +200,10 @@ export default {
enableLiveAutocompletion: this.autoComplete
});
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
this.tables.forEach(table => {
completions.push({
value: table.name,
meta: table.type,
caption: table.comment
});
});
callback(null, completions);
}
});
if (!this.baseCompleter.length)
this.setBaseCompleters(this.editor.completers.map(el => Object.assign({}, el)));
this.baseCompleter = this.editor.completers;
this.setCustomCompleter();
this.editor.commands.on('afterExec', e => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
@@ -164,13 +226,13 @@ export default {
}).catch(console.log);
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
});
@@ -179,6 +241,22 @@ export default {
this.$emit('update:value', content);
});
this.editor.on('guttermousedown', e => {
const target = e.domEvent.target;
if (target.className.indexOf('ace_gutter-cell') === -1)
return;
if (e.clientX > 25 + target.getBoundingClientRect().left)
return;
const row = e.getDocumentPosition().row;
const breakpoints = e.editor.session.getBreakpoints(row, 0);
if (typeof breakpoints[row] === typeof undefined)
e.editor.session.setBreakpoint(row);
else
e.editor.session.clearBreakpoint(row);
e.stop();
});
if (this.autoFocus) {
setTimeout(() => {
this.editor.focus();
@@ -189,6 +267,33 @@ export default {
setTimeout(() => {
this.editor.resize();
}, 20);
},
methods: {
...mapActions({
setBaseCompleters: 'application/setBaseCompleter'
}),
setCustomCompleter () {
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
this.customCompleter = this.editor.completers;
}
}
};
</script>
@@ -219,4 +324,21 @@ export default {
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight {
color: #e0d00c;
}
.ace_gutter-cell.ace_breakpoint {
&::before {
content: '\F0403';
position: absolute;
left: 3px;
top: 2px;
color: $primary-color;
display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons", sans-serif;
font-size: inherit;
text-rendering: auto;
line-height: inherit;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
</style>

View File

@@ -2,9 +2,9 @@
<div id="footer" class="text-light">
<div class="footer-left-elements">
<ul class="footer-elements">
<li class="footer-element" :title="$t('word.version')">
<i class="mdi mdi-18px mdi-memory mr-1" />
<small>{{ appVersion }}</small>
<li class="footer-element">
<i class="mdi mdi-18px mdi-database mr-1" />
<small>{{ versionString }}</small>
</li>
</ul>
</div>
@@ -34,9 +34,18 @@ export default {
name: 'TheFooter',
computed: {
...mapGetters({
appName: 'application/appName',
workspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
appVersion: 'application/appVersion'
})
}),
version () {
return this.getWorkspace(this.workspace) ? this.getWorkspace(this.workspace).version : null;
},
versionString () {
if (this.version)
return `${this.version.name} ${this.version.number} (${this.version.arch} ${this.version.os})`;
return '';
}
},
methods: {
...mapActions({

View File

@@ -7,20 +7,52 @@
ref="tabWrap"
class="tab tab-block column col-12"
>
<li class="tab-item d-none">
<a class="tab-link workspace-tools-link">
<li class="tab-item dropdown tools-dropdown">
<a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
>
<i class="mdi mdi-24px mdi-tools" />
</a>
<ul class="menu text-left text-uppercase">
<li v-if="workspace.customizations.processesList" class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.variables"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.usersManagement"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span>
</a>
</li>
</ul>
</li>
<li
v-if="schemaChild"
v-if="schemaChild && isSettingSupported"
class="tab-item"
:class="{'active': selectedTab === 'prop'}"
@click="selectTab({uid: workspace.uid, tab: 'prop'})"
>
<a class="tab-link">
<i class="mdi mdi-18px mdi-tune mr-1" />
<span :title="schemaChild">{{ $t('word.properties').toUpperCase() }}: {{ schemaChild }}</span>
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="schemaChild">{{ $t('word.settings').toUpperCase() }}: {{ schemaChild }}</span>
</a>
</li>
<li
@@ -89,6 +121,18 @@
:connection="connection"
:routine="workspace.breadcrumbs.procedure"
/>
<WorkspacePropsTabFunction
v-show="selectedTab === 'prop' && workspace.breadcrumbs.function"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:function="workspace.breadcrumbs.function"
/>
<WorkspacePropsTabScheduler
v-show="selectedTab === 'prop' && workspace.breadcrumbs.scheduler"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:scheduler="workspace.breadcrumbs.scheduler"
/>
<WorkspaceTableTab
v-show="selectedTab === 'data'"
:connection="connection"
@@ -97,11 +141,16 @@
<WorkspaceQueryTab
v-for="tab of queryTabs"
:key="tab.uid"
:tab-uid="tab.uid"
:tab="tab"
:is-selected="selectedTab === tab.uid"
:connection="connection"
/>
</div>
<ModalProcessesList
v-if="isProcessesModal"
:connection="connection"
@close="hideProcessesModal"
/>
</div>
</template>
@@ -115,6 +164,9 @@ import WorkspacePropsTab from '@/components/WorkspacePropsTab';
import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
import ModalProcessesList from '@/components/ModalProcessesList';
export default {
name: 'Workspace',
@@ -125,14 +177,18 @@ export default {
WorkspacePropsTab,
WorkspacePropsTabView,
WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine
WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabScheduler,
ModalProcessesList
},
props: {
connection: Object
},
data () {
return {
hasWheelEvent: false
hasWheelEvent: false,
isProcessesModal: false
};
},
computed: {
@@ -146,15 +202,31 @@ export default {
isSelected () {
return this.selectedWorkspace === this.connection.uid;
},
isSettingSupported () {
if (this.workspace.breadcrumbs.table && this.workspace.customizations.tableSettings) return true;
if (this.workspace.breadcrumbs.view && this.workspace.customizations.viewSettings) return true;
if (this.workspace.breadcrumbs.trigger && this.workspace.customizations.triggerSettings) return true;
if (this.workspace.breadcrumbs.procedure && this.workspace.customizations.routineSettings) return true;
if (this.workspace.breadcrumbs.function && this.workspace.customizations.functionSettings) return true;
if (this.workspace.breadcrumbs.scheduler && this.workspace.customizations.schedulerSettings) return true;
return false;
},
selectedTab () {
if (
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
) ||
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.selected_tab === 'data'
)
)
return this.queryTabs[0].uid;
@@ -198,7 +270,7 @@ export default {
removeTab: 'workspaces/removeTab'
}),
addTab () {
this.newTab(this.connection.uid);
this.newTab({ uid: this.connection.uid });
if (!this.hasWheelEvent) {
this.$refs.tabWrap.addEventListener('wheel', e => {
@@ -211,6 +283,12 @@ export default {
closeTab (tUid) {
if (this.queryTabs.length === 1) return;
this.removeTab({ uid: this.connection.uid, tab: tUid });
},
showProcessesModal () {
this.isProcessesModal = true;
},
hideProcessesModal () {
this.isProcessesModal = false;
}
}
};
@@ -275,6 +353,48 @@ export default {
opacity: 1;
}
&.tools-dropdown {
.tab-link:focus {
color: $primary-color;
opacity: 1;
outline: 0;
box-shadow: none;
}
.menu {
min-width: 100%;
.menu-item a {
border-radius: 0.1rem;
color: inherit;
display: block;
margin: 0 -0.4rem;
padding: 0.2rem 0.4rem;
text-decoration: none;
white-space: nowrap;
border: 0;
&:hover {
color: $primary-color;
background: $bg-color-gray;
}
.tool-icon {
line-height: 1;
display: inline-block;
font-size: 20px;
}
}
}
z-index: 9;
position: absolute;
}
&.tools-dropdown + .tab-item {
margin-left: 56px;
}
.workspace-tools-link {
padding-bottom: 0;
padding-top: 0.3rem;

View File

@@ -11,7 +11,7 @@
<span v-if="workspace.connected" class="workspace-explorebar-tools">
<i
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewDatabase')"
:title="$t('message.createNewSchema')"
@click="showNewDBModal"
/>
<i
@@ -27,24 +27,35 @@
/>
</span>
</div>
<div class="workspace-explorebar-search">
<div v-if="workspace.connected" class="has-icon-right">
<input
v-model="searchTerm"
class="form-input input-sm"
type="text"
:placeholder="$t('message.searchForElements')"
>
<i class="form-icon mdi mdi-magnify mdi-18px" />
</div>
</div>
<WorkspaceConnectPanel
v-if="!workspace.connected"
class="workspace-explorebar-body"
:connection="connection"
/>
<div v-else class="workspace-explorebar-body">
<WorkspaceExploreBarDatabase
<WorkspaceExploreBarSchema
v-for="db of workspace.structure"
:key="db.name"
:database="db"
:connection="connection"
@show-database-context="openDatabaseContext"
@show-schema-context="openSchemaContext"
@show-table-context="openTableContext"
@show-misc-context="openMiscContext"
/>
</div>
</div>
<ModalNewDatabase
<ModalNewSchema
v-if="isNewDBModal"
@close="hideNewDBModal"
@reload="refresh"
@@ -73,6 +84,18 @@
@close="hideCreateRoutineModal"
@open-create-routine-editor="openCreateRoutineEditor"
/>
<ModalNewFunction
v-if="isNewFunctionModal"
:workspace="workspace"
@close="hideCreateFunctionModal"
@open-create-function-editor="openCreateFunctionEditor"
/>
<ModalNewScheduler
v-if="isNewSchedulerModal"
:workspace="workspace"
@close="hideCreateSchedulerModal"
@open-create-scheduler-editor="openCreateSchedulerEditor"
/>
<DatabaseContext
v-if="isDatabaseContext"
:selected-database="selectedDatabase"
@@ -82,6 +105,8 @@
@show-create-view-modal="showCreateViewModal"
@show-create-trigger-modal="showCreateTriggerModal"
@show-create-routine-modal="showCreateRoutineModal"
@show-create-function-modal="showCreateFunctionModal"
@show-create-scheduler-modal="showCreateSchedulerModal"
@reload="refresh"
/>
<TableContext
@@ -103,37 +128,42 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import _ from 'lodash';// TODO: remove
import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase';
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext';
import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema';
import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext';
import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import ModalNewDatabase from '@/components/ModalNewDatabase';
import ModalNewSchema from '@/components/ModalNewSchema';
import ModalNewTable from '@/components/ModalNewTable';
import ModalNewView from '@/components/ModalNewView';
import ModalNewTrigger from '@/components/ModalNewTrigger';
import ModalNewRoutine from '@/components/ModalNewRoutine';
import ModalNewFunction from '@/components/ModalNewFunction';
import ModalNewScheduler from '@/components/ModalNewScheduler';
export default {
name: 'WorkspaceExploreBar',
components: {
WorkspaceConnectPanel,
WorkspaceExploreBarDatabase,
WorkspaceExploreBarSchema,
DatabaseContext,
TableContext,
MiscContext,
ModalNewDatabase,
ModalNewSchema,
ModalNewTable,
ModalNewView,
ModalNewTrigger,
ModalNewRoutine
ModalNewRoutine,
ModalNewFunction,
ModalNewScheduler
},
props: {
connection: Object,
@@ -148,8 +178,12 @@ export default {
isNewViewModal: false,
isNewTriggerModal: false,
isNewRoutineModal: false,
isNewFunctionModal: false,
isNewSchedulerModal: false,
localWidth: null,
explorebarWidthInterval: null,
searchTermInterval: null,
isDatabaseContext: false,
isTableContext: false,
isMiscContext: false,
@@ -160,7 +194,8 @@ export default {
selectedDatabase: '',
selectedTable: null,
selectedMisc: null
selectedMisc: null,
searchTerm: ''
};
},
computed: {
@@ -177,11 +212,22 @@ export default {
}
},
watch: {
localWidth: _.debounce(function (val) {
this.changeExplorebarSize(val);
}, 500),
localWidth (val) {
clearTimeout(this.explorebarWidthInterval);
this.explorebarWidthInterval = setTimeout(() => {
this.changeExplorebarSize(val);
}, 500);
},
isSelected (val) {
if (val) this.localWidth = this.explorebarSize;
},
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => {
this.setSearchTerm(this.searchTerm);
}, 200);
}
},
created () {
@@ -190,7 +236,7 @@ export default {
mounted () {
const resizer = this.$refs.resizer;
resizer.addEventListener('mousedown', (e) => {
resizer.addEventListener('mousedown', e => {
e.preventDefault();
window.addEventListener('mousemove', this.resize);
@@ -203,6 +249,7 @@ export default {
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
selectTab: 'workspaces/selectTab',
setSearchTerm: 'workspaces/setSearchTerm',
addNotification: 'notifications/addNotification',
changeExplorebarSize: 'settings/changeExplorebarSize'
}),
@@ -252,8 +299,8 @@ export default {
else
this.addNotification({ status: 'error', message: response });
},
openDatabaseContext (payload) {
this.selectedDatabase = payload.database;
openSchemaContext (payload) {
this.selectedDatabase = payload.schema;
this.databaseContextEvent = payload.event;
this.isDatabaseContext = true;
},
@@ -344,6 +391,52 @@ export default {
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateFunctionModal () {
this.closeDatabaseContext();
this.isNewFunctionModal = true;
},
hideCreateFunctionModal () {
this.isNewFunctionModal = false;
},
showCreateSchedulerModal () {
this.closeDatabaseContext();
this.isNewSchedulerModal = true;
},
hideCreateSchedulerModal () {
this.isNewSchedulerModal = false;
},
async openCreateFunctionEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Functions.createFunction(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, function: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
},
async openCreateSchedulerEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Schedulers.createScheduler(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, scheduler: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
}
}
};
@@ -409,9 +502,40 @@ export default {
}
}
.workspace-explorebar-search {
width: 100%;
display: flex;
justify-content: space-between;
font-size: 0.6rem;
height: 28px;
.has-icon-right {
width: 100%;
padding: 0.1rem;
.form-icon {
opacity: 0.5;
transition: opacity 0.2s;
}
.form-input {
height: 1.2rem;
padding-left: 0.2rem;
&:focus + .form-icon {
opacity: 0.9;
}
&::placeholder {
opacity: 0.6;
}
}
}
}
.workspace-explorebar-body {
width: 100%;
height: calc((100vh - 30px) - #{$excluding-size});
height: calc((100vh - 58px) - #{$excluding-size});
overflow: overlay;
padding: 0 0.1rem;
}

View File

@@ -4,9 +4,9 @@
@close-context="closeContext"
>
<div
v-if="selectedMisc.type === 'procedure'"
class="context-element disabled"
@click="showRunModal"
v-if="['procedure', 'function'].includes(selectedMisc.type)"
class="context-element"
@click="runElementCheck"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
</div>
@@ -29,6 +29,12 @@
</div>
</div>
</ConfirmModal>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localElement"
@confirm="runElement"
@close="hideAskParamsModal"
/>
</BaseContextMenu>
</template>
@@ -36,14 +42,18 @@
import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
export default {
name: 'WorkspaceExploreBarMiscContext',
components: {
BaseContextMenu,
ConfirmModal
ConfirmModal,
ModalAskParameters
},
props: {
contextEvent: MouseEvent,
@@ -52,7 +62,9 @@ export default {
data () {
return {
isDeleteModal: false,
isRunModal: false
isRunModal: false,
isAskingParameters: false,
localElement: {}
};
},
computed: {
@@ -69,6 +81,10 @@ export default {
return this.$t('message.deleteTrigger');
case 'procedure':
return this.$t('message.deleteRoutine');
case 'function':
return this.$t('message.deleteFunction');
case 'scheduler':
return this.$t('message.deleteScheduler');
default:
return '';
}
@@ -77,7 +93,8 @@ export default {
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
@@ -88,11 +105,12 @@ export default {
hideDeleteModal () {
this.isDeleteModal = false;
},
showRunModal () {
this.isRunModal = true;
showAskParamsModal () {
this.isAskingParameters = true;
},
hideRunModal () {
this.isRunModal = false;
hideAskParamsModal () {
this.isAskingParameters = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
@@ -114,12 +132,18 @@ export default {
routine: this.selectedMisc.name
});
break;
// case 'schedules':
// res = await Tables.dropScheduler({
// uid: this.selectedWorkspace,
// scheduler: this.selectedMisc.name
// });
// break;
case 'function':
res = await Functions.dropFunction({
uid: this.selectedWorkspace,
func: this.selectedMisc.name
});
break;
case 'scheduler':
res = await Schedulers.dropScheduler({
uid: this.selectedWorkspace,
scheduler: this.selectedMisc.name
});
break;
}
const { status, response } = res;
@@ -136,6 +160,105 @@ export default {
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
runElementCheck () {
if (this.selectedMisc.type === 'procedure')
this.runRoutineCheck();
else if (this.selectedMisc.type === 'function')
this.runFunctionCheck();
},
runElement (params) {
if (this.selectedMisc.type === 'procedure')
this.runRoutine(params);
else if (this.selectedMisc.type === 'function')
this.runFunction(params);
},
async runRoutineCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
routine: this.workspace.breadcrumbs.procedure
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success')
this.localElement = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length)
this.showAskParamsModal();
else
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql;
switch (this.workspace.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.closeContext();
},
async runFunctionCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
func: this.workspace.breadcrumbs.function
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success')
this.localElement = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql;
switch (this.workspace.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.closeContext();
}
}
};

View File

@@ -3,10 +3,11 @@
<summary
class="accordion-header database-name"
:class="{'text-bold': breadcrumbs.schema === database.name}"
@click="changeBreadcrumbs({schema: database.name, table: null})"
@contextmenu.prevent="showDatabaseContext($event, database.name)"
@click="selectSchema(database.name)"
@contextmenu.prevent="showSchemaContext($event, database.name)"
>
<i class="icon mdi mdi-18px mdi-chevron-right" />
<div v-if="isLoading" class="icon loading" />
<i v-else class="icon mdi mdi-18px mdi-chevron-right" />
<i class="database-icon mdi mdi-18px mdi-database mr-1" />
<span>{{ database.name }}</span>
</summary>
@@ -14,7 +15,7 @@
<div class="database-tables">
<ul class="menu menu-nav pt-0">
<li
v-for="table of database.tables"
v-for="table of filteredTables"
:key="table.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@@ -23,7 +24,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span>{{ table.name }}</span>
<span v-html="highlightWord(table.name)" />
</a>
<div
v-if="table.type === 'table'"
@@ -36,7 +37,7 @@
</ul>
</div>
<div v-if="database.triggers.length" class="database-misc">
<div v-if="filteredTriggers.length && customizations.triggers" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}">
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
@@ -46,7 +47,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="trigger of database.triggers"
v-for="trigger of filteredTriggers"
:key="trigger.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@@ -55,7 +56,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<span>{{ trigger.name }}</span>
<span v-html="highlightWord(trigger.name)" />
</a>
</li>
</ul>
@@ -64,7 +65,7 @@
</details>
</div>
<div v-if="database.procedures.length" class="database-misc">
<div v-if="filteredProcedures.length && customizations.routines" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}">
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
@@ -74,7 +75,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="procedure of database.procedures"
v-for="procedure of filteredProcedures"
:key="procedure.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@@ -83,7 +84,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span>{{ procedure.name }}</span>
<span v-html="highlightWord(procedure.name)" />
</a>
</li>
</ul>
@@ -92,7 +93,7 @@
</details>
</div>
<div v-if="database.functions.length" class="database-misc">
<div v-if="filteredFunctions.length && customizations.functions" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}">
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
@@ -102,7 +103,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="func of database.functions"
v-for="func of filteredFunctions"
:key="func.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@@ -111,7 +112,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" />
<span>{{ func.name }}</span>
<span v-html="highlightWord(func.name)" />
</a>
</li>
</ul>
@@ -120,7 +121,7 @@
</details>
</div>
<div v-if="database.schedulers.length" class="database-misc">
<div v-if="filteredSchedulers.length && customizations.schedulers" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}">
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
@@ -130,7 +131,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="scheduler of database.schedulers"
v-for="scheduler of filteredSchedulers"
:key="scheduler.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@@ -139,7 +140,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<span>{{ scheduler.name }}</span>
<span v-html="highlightWord(scheduler.name)" />
</a>
</li>
</ul>
@@ -156,18 +157,49 @@ import { mapActions, mapGetters } from 'vuex';
import { formatBytes } from 'common/libs/formatBytes';
export default {
name: 'WorkspaceExploreBarDatabase',
name: 'WorkspaceExploreBarSchema',
props: {
database: Object,
connection: Object
},
data () {
return {
isLoading: false
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getLoadedSchemas: 'workspaces/getLoadedSchemas',
getWorkspace: 'workspaces/getWorkspace',
getSearchTerm: 'workspaces/getSearchTerm'
}),
searchTerm () {
return this.getSearchTerm(this.connection.uid);
},
filteredTables () {
return this.database.tables.filter(table => table.name.search(this.searchTerm) >= 0);
},
filteredTriggers () {
return this.database.triggers.filter(trigger => trigger.name.search(this.searchTerm) >= 0);
},
filteredProcedures () {
return this.database.procedures.filter(procedure => procedure.name.search(this.searchTerm) >= 0);
},
filteredFunctions () {
return this.database.functions.filter(func => func.name.search(this.searchTerm) >= 0);
},
filteredSchedulers () {
return this.database.schedulers.filter(scheduler => scheduler.name.search(this.searchTerm) >= 0);
},
breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs;
},
customizations () {
return this.getWorkspace(this.connection.uid).customizations;
},
loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid);
},
maxSize () {
return this.database.tables.reduce((acc, curr) => {
if (curr.size > acc) acc = curr.size;
@@ -180,12 +212,22 @@ export default {
},
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
refreshSchema: 'workspaces/refreshSchema'
}),
formatBytes,
showDatabaseContext (event, database) {
this.changeBreadcrumbs({ schema: database, table: null });
this.$emit('show-database-context', { event, database });
async selectSchema (schema) {
if (!this.loadedSchemas.has(schema)) {
this.isLoading = true;
await this.refreshSchema({ uid: this.connection.uid, schema });
this.isLoading = false;
}
this.changeBreadcrumbs({ schema, table: null });
},
showSchemaContext (event, schema) {
this.selectSchema(schema);
this.$emit('show-schema-context', { event, schema });
},
showTableContext (event, table) {
this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name });
@@ -202,6 +244,14 @@ export default {
setBreadcrumbs (payload) {
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
this.changeBreadcrumbs(payload);
},
highlightWord (string) {
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary">$1</span>');
}
else
return string;
}
}
};
@@ -209,6 +259,13 @@ export default {
<style lang="scss">
.workspace-explorebar-database {
.database-name {
position: sticky;
top: 0;
background: $bg-color-gray;
z-index: 2;
}
.database-name,
.misc-name,
a.table-name {
@@ -230,6 +287,16 @@ export default {
.misc-icon {
opacity: 0.7;
}
.loading {
height: 18px;
width: 18px;
&::after {
height: 0.6rem;
width: 0.6rem;
}
}
}
.misc-name {
@@ -242,7 +309,7 @@ export default {
.misc-name {
&:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
background: $bg-color-light;
border-radius: 2px;
}
}

View File

@@ -7,27 +7,55 @@
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div class="context-element" @click="showCreateTableModal">
<div
v-if="workspace.customizations.tableAdd"
class="context-element"
@click="showCreateTableModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div>
<div class="context-element" @click="showCreateViewModal">
<div
v-if="workspace.customizations.viewAdd"
class="context-element"
@click="showCreateViewModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
</div>
<div class="context-element" @click="showCreateTriggerModal">
<div
v-if="workspace.customizations.triggerAdd"
class="context-element"
@click="showCreateTriggerModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div>
<div class="context-element" @click="showCreateRoutineModal">
<div
v-if="workspace.customizations.routineAdd"
class="context-element"
@click="showCreateRoutineModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div>
<div class="context-element disabled" @click="false">
<div
v-if="workspace.customizations.functionAdd"
class="context-element"
@click="showCreateFunctionModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div>
<div class="context-element disabled" @click="false">
<div
v-if="workspace.customizations.schedulerAdd"
class="context-element"
@click="showCreateSchedulerModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div>
</div>
</div>
<div class="context-element" @click="showEditModal">
<div
v-if="workspace.customizations.schemaEdit"
class="context-element"
@click="showEditModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div>
<div class="context-element" @click="showDeleteModal">
@@ -36,12 +64,12 @@
<ConfirmModal
v-if="isDeleteModal"
@confirm="deleteDatabase"
@confirm="deleteSchema"
@hide="hideDeleteModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteDatabase') }}
<i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteSchema') }}
</div>
</template>
<div slot="body">
@@ -50,7 +78,7 @@
</div>
</div>
</ConfirmModal>
<ModalEditDatabase
<ModalEditSchema
v-if="isEditModal"
:selected-database="selectedDatabase"
@close="hideEditModal"
@@ -62,15 +90,15 @@
import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditDatabase from '@/components/ModalEditDatabase';
import Database from '@/ipc-api/Database';
import ModalEditSchema from '@/components/ModalEditSchema';
import Schema from '@/ipc-api/Schema';
export default {
name: 'WorkspaceExploreBarDatabaseContext',
name: 'WorkspaceExploreBarSchemaContext',
components: {
BaseContextMenu,
ConfirmModal,
ModalEditDatabase
ModalEditSchema
},
props: {
contextEvent: MouseEvent,
@@ -108,6 +136,12 @@ export default {
showCreateRoutineModal () {
this.$emit('show-create-routine-modal');
},
showCreateFunctionModal () {
this.$emit('show-create-function-modal');
},
showCreateSchedulerModal () {
this.$emit('show-create-scheduler-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},
@@ -124,9 +158,9 @@ export default {
closeContext () {
this.$emit('close-context');
},
async deleteDatabase () {
async deleteSchema () {
try {
const { status, response } = await Database.deleteDatabase({
const { status, response } = await Schema.deleteSchema({
uid: this.selectedWorkspace,
database: this.selectedDatabase
});

View File

@@ -0,0 +1,180 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" /> {{ $t('word.options') }} "{{ localOptions.name }}"
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="optionsProxy.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="optionsProxy.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.returns') }}
</label>
<div class="column">
<div class="input-group">
<select
v-model="optionsProxy.returns"
class="form-select text-uppercase"
style="width: 0;"
>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="optionsProxy.returns === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
<input
v-model="optionsProxy.returnsLength"
class="form-input"
type="number"
min="0"
>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="optionsProxy.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="optionsProxy.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="optionsProxy.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="optionsProxy.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsFunctionOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
}
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
if (!this.isTableNameValid)
this.optionsProxy.name = this.localOptions.name;
this.$emit('options-update', this.optionsProxy);
}
}
};
</script>

View File

@@ -0,0 +1,277 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> {{ $t('word.parameters') }} "{{ func }}"
</div>
</template>
<div :slot="'body'">
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')"
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
<div ref="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param._id}"
@click="selectParameter($event, param._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-hexagon mdi-24px" :class="typeClass(param.type)" />
</div>
</div>
<div class="tile-content">
<div class="tile-title">
{{ param.name }}
</div>
<small class="tile-subtitle text-gray">{{ param.type }}{{ param.length ? `(${param.length})` : '' }}</small>
</div>
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeParameter(param._id)"
>
<i class="mdi mdi-close" />
</button>
</div>
</div>
</div>
</div>
</div>
<div class="column col-7 pl-2 editor-col">
<form
v-if="selectedParamObj"
:style="{ height: modalInnerHeight + 'px'}"
class="form-horizontal"
>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.name') }}
</label>
<div class="column">
<input
v-model="selectedParamObj.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.type') }}
</label>
<div class="column">
<select v-model="selectedParamObj.type" class="form-select text-uppercase">
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="selectedParamObj.type.toUpperCase() === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.length') }}
</label>
<div class="column">
<input
v-model="selectedParamObj.length"
class="form-input"
type="number"
min="0"
>
</div>
</div>
</form>
<div v-if="!parametersProxy.length" class="empty">
<div class="empty-icon">
<i class="mdi mdi-dots-horizontal mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoParameters') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addParameter">
{{ $t('message.createNewParameter') }}
</button>
</div>
</div>
</div>
</div>
</div>
</ConfirmModal>
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineParamsModal',
components: {
ConfirmModal
},
props: {
localParameters: Array,
func: String,
workspace: Object
},
data () {
return {
parametersProxy: [],
isOptionsChanging: false,
selectedParam: '',
modalInnerHeight: 400,
i: 1
};
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param._id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
}
},
mounted () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (this.parametersProxy.length)
this.resetSelectedID();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
},
destroyed () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, uid) {
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`,
type: 'INT',
context: 'IN',
length: 10
}];
if (this.parametersProxy.length === 1)
this.resetSelectedID();
setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20);
},
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID();
},
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (!this.parametersProxy.some(param => param.name === this.selectedParam))
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
}
}
};
</script>
<style lang="scss" scoped>
.tile {
border-radius: 2px;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;
.tile-action {
opacity: 0;
transition: opacity 0.2s;
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-param {
background: $bg-color-light;
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 300px;
overflow: auto;
}
.remove-field .mdi {
pointer-events: none;
}
</style>

View File

@@ -2,7 +2,7 @@
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmIndexesChange"
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
@@ -34,14 +34,14 @@
<div ref="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param.name"
:key="param._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}"
@click="selectParameter($event, param.name)"
:class="{'selected-param': selectedParam === param._id}"
@click="selectParameter($event, param._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" />
<i class="mdi mdi-hexagon mdi-24px" :class="typeClass(param.type)" />
</div>
</div>
<div class="tile-content">
@@ -54,7 +54,7 @@
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeParameter(param.name)"
@click.prevent="removeParameter(param._id)"
>
<i class="mdi mdi-close" />
</button>
@@ -114,6 +114,7 @@
v-model="selectedParamObj.length"
class="form-input"
type="number"
min="0"
>
</div>
</div>
@@ -169,6 +170,7 @@
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
@@ -192,7 +194,7 @@ export default {
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam);
return this.parametersProxy.find(param => param._id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
@@ -212,12 +214,17 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, name) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field'))
this.selectedParam = name;
selectParameter (event, uid) {
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
@@ -226,6 +233,7 @@ export default {
},
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`,
type: 'INT',
context: 'IN',
@@ -239,8 +247,8 @@ export default {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20);
},
removeParameter (name) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name);
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID();
@@ -253,7 +261,7 @@ export default {
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : '';
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
}
}
};

View File

@@ -0,0 +1,192 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-timer mr-1" /> {{ $t('word.timing') }} "{{ localOptions.name }}"
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.execution') }}
</label>
<div class="column">
<select
ref="firstInput"
v-model="optionsProxy.execution"
class="form-select"
>
<option>EVERY</option>
<option>ONCE</option>
</select>
</div>
</div>
<div v-if="optionsProxy.execution === 'EVERY'">
<div class="form-group">
<div class="col-4" />
<div class="column">
<div class="input-group">
<input
v-model="optionsProxy.every[0]"
class="form-input"
type="text"
@keypress="isNumberOrMinus($event)"
>
<select
v-model="optionsProxy.every[1]"
class="form-select text-uppercase"
style="width: 0;"
>
<option>YEAR</option>
<option>QUARTER</option>
<option>MONTH</option>
<option>WEEK</option>
<option>DAY</option>
<option>HOUR</option>
<option>MINUTE</option>
<option>SECOND</option>
<option>YEAR_MONTH</option>
<option>DAY_HOUR</option>
<option>DAY_MINUTE</option>
<option>DAY_SECOND</option>
<option>HOUR_MINUTE</option>
<option>HOUR_SECOND</option>
<option>MINUTE_SECOND</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.starts') }}
</label>
<div class="column">
<div class="input-group">
<label class="form-checkbox">
<input v-model="hasStart" type="checkbox"><i class="form-icon" />
</label>
<input
v-model="optionsProxy.starts"
v-mask="'####-##-## ##:##:##'"
type="text"
class="form-input"
>
<span class="input-group-addon p-vcentered">
<i class="form-icon mdi mdi-calendar" />
</span>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.ends') }}
</label>
<div class="column">
<div class="input-group">
<label class="form-checkbox">
<input v-model="hasEnd" type="checkbox"><i class="form-icon" />
</label>
<input
v-model="optionsProxy.ends"
v-mask="'####-##-## ##:##:##'"
type="text"
class="form-input"
>
<span class="input-group-addon p-vcentered">
<i class="form-icon mdi mdi-calendar" />
</span>
</div>
</div>
</div>
</div>
<div v-else>
<div class="form-group">
<div class="col-4" />
<div class="column">
<div class="input-group">
<input
v-model="optionsProxy.at"
v-mask="'####-##-## ##:##:##'"
type="text"
class="form-input"
>
<span class="input-group-addon p-vcentered">
<i class="form-icon mdi mdi-calendar" />
</span>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline mt-2">
<input v-model="optionsProxy.preserve" type="checkbox"><i class="form-icon" /> {{ $t('message.preserveOnCompletion') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import { VueMaskDirective } from 'v-mask';
import moment from 'moment';
export default {
name: 'WorkspacePropsSchedulerTimingModal',
components: {
ConfirmModal
},
directives: {
mask: VueMaskDirective
},
props: {
localOptions: Object,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false,
hasStart: false,
hasEnd: false
};
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
this.hasStart = !!this.optionsProxy.starts;
this.hasEnd = !!this.optionsProxy.ends;
if (!this.optionsProxy.at) this.optionsProxy.at = moment().format('YYYY-MM-DD HH:mm:ss');
if (!this.optionsProxy.starts) this.optionsProxy.starts = moment().format('YYYY-MM-DD HH:mm:ss');
if (!this.optionsProxy.ends) this.optionsProxy.ends = moment().format('YYYY-MM-DD HH:mm:ss');
if (!this.optionsProxy.every.length) this.optionsProxy.every = ['1', 'DAY'];
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
if (!this.hasStart) this.optionsProxy.starts = '';
if (!this.hasEnd) this.optionsProxy.ends = '';
this.$emit('options-update', this.optionsProxy);
},
isNumberOrMinus (event) {
if (!/\d/.test(event.key) && event.key !== '-')
return event.preventDefault();
}
}
};
</script>

View File

@@ -51,7 +51,8 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12">
<div class="workspace-query-results column col-12 p-relative">
<BaseLoader v-if="isLoading" />
<WorkspacePropsTable
v-if="localFields"
ref="indexTable"
@@ -106,6 +107,7 @@
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
@@ -114,6 +116,7 @@ import WorkspacePropsForeignModal from '@/components/WorkspacePropsForeignModal'
export default {
name: 'WorkspacePropsTab',
components: {
BaseLoader,
WorkspacePropsTable,
WorkspacePropsOptionsModal,
WorkspacePropsIndexesModal,
@@ -126,7 +129,7 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
isOptionsModal: false,
isIndexesModal: false,
@@ -205,8 +208,10 @@ export default {
}),
async getFieldsData () {
if (!this.table) return;
this.localFields = [];
this.newFieldsCounter = 0;
this.isQuering = true;
this.isLoading = true;
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
const params = {
@@ -281,7 +286,7 @@ export default {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;
@@ -432,7 +437,7 @@ export default {
schema: this.schema,
table: this.table,
numPrecision: null,
numLength: null,
numLength: 11,
datePrecision: null,
charLength: null,
nullable: false,

View File

@@ -0,0 +1,315 @@
<template>
<div class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm"
:disabled="isChanged"
@click="runFunctionCheck"
>
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<span>{{ $t('word.parameters') }}</span>
<i class="mdi mdi-24px mdi-dots-horizontal ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<span>{{ $t('word.options') }}</span>
<i class="mdi mdi-24px mdi-cogs ml-1" />
</button>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
<QueryEditor
v-if="isSelected"
ref="queryEditor"
:value.sync="localFunction.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspacePropsFunctionOptionsModal
v-if="isOptionsModal"
:local-options="localFunction"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsFunctionParamsModal
v-if="isParamsModal"
:local-parameters="localFunction.parameters"
:workspace="workspace"
:func="localFunction.name"
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localFunction"
@confirm="runFunction"
@close="hideAskParamsModal"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsFunctionOptionsModal from '@/components/WorkspacePropsFunctionOptionsModal';
import WorkspacePropsFunctionParamsModal from '@/components/WorkspacePropsFunctionParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspacePropsTabFunction',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsFunctionOptionsModal,
WorkspacePropsFunctionParamsModal,
ModalAskParameters
},
props: {
connection: Object,
function: String
},
data () {
return {
tabUid: 'prop',
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
localFunction: { sql: '' },
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
},
schema () {
return this.workspace.breadcrumbs.schema;
},
isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction ? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
async function () {
if (this.isSelected) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
}
},
async isSelected (val) {
if (val && this.lastFunction !== this.function) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
}
},
isChanged (val) {
if (this.isSelected && this.lastFunction === this.function && this.function !== null)
this.setUnsavedChanges(val);
}
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
async getFunctionData () {
if (!this.function) return;
this.isLoading = true;
this.localFunction = { sql: '' };
const params = {
uid: this.connection.uid,
schema: this.schema,
func: this.workspace.breadcrumbs.function
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success') {
this.originalFunction = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => {
param._id = uidGen();
return param;
})];
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.sqlProxy = this.localFunction.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor();
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
func: {
...this.localFunction,
oldName: this.originalFunction.name
}
};
try {
const { status, response } = await Functions.alterFunction(params);
if (status === 'success') {
const oldName = this.originalFunction.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localFunction.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, function: this.localFunction.name });
}
this.getFunctionData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
optionsUpdate (options) {
this.localFunction = options;
},
parametersUpdate (parameters) {
this.localFunction = { ...this.localFunction, parameters };
},
runFunctionCheck () {
if (this.localFunction.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql;
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
}
}
};
</script>

View File

@@ -25,9 +25,9 @@
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm disabled"
class="btn btn-dark btn-sm"
:disabled="isChanged"
@click="false"
@click="runRoutineCheck"
>
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
@@ -43,10 +43,12 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor
v-if="isSelected"
v-show="isSelected"
:key="`${routine}-${_uid}`"
ref="queryEditor"
:value.sync="localRoutine.sql"
:workspace="workspace"
@@ -69,22 +71,33 @@
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localRoutine"
@confirm="runRoutine"
@close="hideAskParamsModal"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Routines from '@/ipc-api/Routines';
export default {
name: 'WorkspacePropsTabRoutine',
components: {
QueryEditor,
BaseLoader,
WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal
WorkspacePropsRoutineParamsModal,
ModalAskParameters
},
props: {
connection: Object,
@@ -93,10 +106,11 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalRoutine: null,
localRoutine: { sql: '' },
lastRoutine: null,
@@ -162,11 +176,13 @@ export default {
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
async getRoutineData () {
if (!this.routine) return;
this.isQuering = true;
this.localRoutine = { sql: '' };
this.isLoading = true;
const params = {
uid: this.connection.uid,
@@ -178,6 +194,12 @@ export default {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') {
this.originalRoutine = response;
this.originalRoutine.parameters = [...this.originalRoutine.parameters.map(param => {
param._id = uidGen();
return param;
})];
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.sqlProxy = this.localRoutine.sql;
}
@@ -189,7 +211,7 @@ export default {
}
this.resizeQueryEditor();
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;
@@ -245,6 +267,31 @@ export default {
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
runRoutineCheck () {
if (this.localRoutine.parameters.length)
this.showAskParamsModal();
else
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql;
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.originalRoutine.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
@@ -256,6 +303,12 @@ export default {
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
}
}
};

View File

@@ -0,0 +1,316 @@
<template>
<div class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showTimingModal">
<span>{{ $t('word.timing') }}</span>
<i class="mdi mdi-24px mdi-timer ml-1" />
</button>
</div>
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
v-model="localScheduler.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localScheduler.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalScheduler.definer">
{{ originalScheduler.definer.replaceAll('`', '') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="column col-4">
<div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label>
<input
v-model="localScheduler.comment"
class="form-input"
type="text"
>
</div>
</div>
</div>
<div class="columns">
<div class="column">
<div class="form-group">
<label class="form-label mr-2">{{ $t('word.state') }}</label>
<label class="form-radio form-inline">
<input
v-model="localScheduler.state"
type="radio"
name="state"
value="ENABLE"
><i class="form-icon" /> ENABLE
</label>
<label class="form-radio form-inline">
<input
v-model="localScheduler.state"
type="radio"
name="state"
value="DISABLE"
><i class="form-icon" /> DISABLE
</label>
<label class="form-radio form-inline">
<input
v-model="localScheduler.state"
type="radio"
name="state"
value="DISABLE ON SLAVE"
><i class="form-icon" /> DISABLE ON SLAVE
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localScheduler.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspacePropsSchedulerTimingModal
v-if="isTimingModal"
:local-options="localScheduler"
:workspace="workspace"
@hide="hideTimingModal"
@options-update="timingUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsSchedulerTimingModal from '@/components/WorkspacePropsSchedulerTimingModal';
import Schedulers from '@/ipc-api/Schedulers';
export default {
name: 'WorkspacePropsTabScheduler',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsSchedulerTimingModal
},
props: {
connection: Object,
scheduler: String
},
data () {
return {
tabUid: 'prop',
isLoading: false,
isSaving: false,
isTimingModal: false,
originalScheduler: null,
localScheduler: { sql: '' },
lastScheduler: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
},
schema () {
return this.workspace.breadcrumbs.schema;
},
isChanged () {
return JSON.stringify(this.originalScheduler) !== JSON.stringify(this.localScheduler);
},
isDefinerInUsers () {
return this.originalScheduler ? this.workspace.users.some(user => this.originalScheduler.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
async scheduler () {
if (this.isSelected) {
await this.getSchedulerData();
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
this.lastScheduler = this.scheduler;
}
},
async isSelected (val) {
if (val && this.lastScheduler !== this.scheduler) {
await this.getSchedulerData();
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
this.lastScheduler = this.scheduler;
}
},
isChanged (val) {
if (this.isSelected && this.lastScheduler === this.scheduler && this.scheduler !== null)
this.setUnsavedChanges(val);
}
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async getSchedulerData () {
if (!this.scheduler) return;
this.isLoading = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
scheduler: this.workspace.breadcrumbs.scheduler
};
try {
const { status, response } = await Schedulers.getSchedulerInformations(params);
if (status === 'success') {
this.originalScheduler = response;
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
this.sqlProxy = this.localScheduler.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor();
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
scheduler: {
...this.localScheduler,
oldName: this.originalScheduler.name
}
};
try {
const { status, response } = await Schedulers.alterScheduler(params);
if (status === 'success') {
const oldName = this.originalScheduler.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localScheduler.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, scheduler: this.localScheduler.name });
}
this.getSchedulerData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
showTimingModal () {
this.isTimingModal = true;
},
hideTimingModal () {
this.isTimingModal = false;
},
timingUpdate (options) {
this.localScheduler = options;
}
}
};
</script>

View File

@@ -26,7 +26,7 @@
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-3">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
@@ -36,7 +36,7 @@
>
</div>
</div>
<div class="column col-3">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
@@ -67,7 +67,7 @@
</div>
</div>
<div class="columns">
<div class="column col-3">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label>
<select v-model="localTrigger.table" class="form-select">
@@ -77,7 +77,7 @@
</select>
</div>
</div>
<div class="column col-3">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label>
<div class="input-group">
@@ -95,10 +95,11 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor
v-if="isSelected"
v-show="isSelected"
ref="queryEditor"
:value.sync="localTrigger.sql"
:workspace="workspace"
@@ -110,13 +111,15 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import Triggers from '@/ipc-api/Triggers';
export default {
name: 'WorkspacePropsTabTrigger',
components: {
BaseLoader,
QueryEditor
},
props: {
@@ -126,7 +129,7 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
originalTrigger: null,
localTrigger: { sql: '' },
@@ -197,7 +200,9 @@ export default {
}),
async getTriggerData () {
if (!this.trigger) return;
this.isQuering = true;
this.localTrigger = { sql: '' };
this.isLoading = true;
const params = {
uid: this.connection.uid,
@@ -220,7 +225,7 @@ export default {
}
this.resizeQueryEditor();
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;

View File

@@ -26,7 +26,7 @@
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-3">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
@@ -36,7 +36,7 @@
>
</div>
</div>
<div class="column col-3">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
@@ -67,7 +67,7 @@
</div>
</div>
<div class="columns">
<div class="column col-2">
<div class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
@@ -90,7 +90,7 @@
</label>
</div>
</div>
<div class="column col-2">
<div class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
@@ -122,7 +122,7 @@
</label>
</div>
</div>
<div class="column col-2">
<div class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio">
@@ -156,7 +156,8 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label>
<QueryEditor
v-if="isSelected"
@@ -172,12 +173,14 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import Views from '@/ipc-api/Views';
export default {
name: 'WorkspacePropsTabView',
components: {
BaseLoader,
QueryEditor
},
props: {
@@ -187,7 +190,7 @@ export default {
data () {
return {
tabUid: 'prop',
isQuering: false,
isLoading: false,
isSaving: false,
originalView: null,
localView: { sql: '' },
@@ -251,7 +254,8 @@ export default {
}),
async getViewData () {
if (!this.view) return;
this.isQuering = true;
this.isLoading = true;
this.localView = { sql: '' };
const params = {
uid: this.connection.uid,
@@ -274,7 +278,7 @@ export default {
}
this.resizeQueryEditor();
this.isQuering = false;
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;

View File

@@ -48,7 +48,7 @@
<span
v-if="!isInlineEditor.type"
class="cell-content text-left"
:class="`type-${lowerCase(localRow.type)}`"
:class="typeClass(localRow.type)"
@click="editON($event, localRow.type.toUpperCase(), 'type')"
>
{{ localRow.type }}
@@ -318,7 +318,7 @@ export default {
getWorkspace: 'workspaces/getWorkspace'
}),
localLength () {
return this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || 0;
return this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || this.localRow.numPrecision || 0;
},
fieldType () {
const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type =>
@@ -378,10 +378,10 @@ export default {
return 'UNKNOWN ' + key;
}
},
lowerCase (val) {
if (val)
return val.toLowerCase();
return val;
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
initLocalRow () {
Object.keys(this.localRow).forEach(key => {
@@ -400,7 +400,7 @@ export default {
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default.replace(/(^')|('$)/g, '');
}
else if (!isNaN(this.localRow.default)) {
else if (!isNaN(this.localRow.default.replace(/[:.-\s]/g, ''))) {
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default;
}

View File

@@ -2,19 +2,23 @@
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<QueryEditor
v-if="isSelected"
v-show="isSelected"
ref="queryEditor"
:auto-focus="true"
:value.sync="query"
:workspace="workspace"
:schema="schema"
:is-selected="isSelected"
:height="editorHeight"
/>
<div ref="resizer" class="query-area-resizer" />
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:class="{'loading':isQuering}"
:disabled="!query"
title="F9"
title="F5"
@click="runQuery(query)"
>
<span>{{ $t('word.run') }}</span>
@@ -22,6 +26,13 @@
</button>
</div>
<div class="workspace-query-info">
<div
v-if="results.length"
class="d-flex"
:title="$t('message.queryDuration')"
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b>
</div>
<div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
</div>
@@ -38,14 +49,16 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12">
<div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" />
<WorkspaceQueryTable
v-if="results"
v-show="!isQuering"
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:tab-uid="tab.uid"
:conn-uid="connection.uid"
:is-selected="isSelected"
mode="query"
@update-field="updateField"
@delete-selected="deleteSelected"
@@ -55,8 +68,9 @@
</template>
<script>
import Database from '@/ipc-api/Database';
import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs';
@@ -64,13 +78,14 @@ import tableTabs from '@/mixins/tableTabs';
export default {
name: 'WorkspaceQueryTab',
components: {
BaseLoader,
QueryEditor,
WorkspaceQueryTable
},
mixins: [tableTabs],
props: {
connection: Object,
tabUid: String,
tab: Object,
isSelected: Boolean
},
data () {
@@ -80,20 +95,41 @@ export default {
isQuering: false,
results: [],
resultsCount: 0,
affectedCount: 0
durationsCount: 0,
affectedCount: 0,
editorHeight: 200
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isWorkspaceSelected () {
return this.workspace.uid === this.selectedWorkspace;
}
},
created () {
this.query = this.tab.content;
window.addEventListener('keydown', this.onKey);
},
mounted () {
const resizer = this.$refs.resizer;
resizer.addEventListener('mousedown', e => {
e.preventDefault();
window.addEventListener('mousemove', this.resize);
window.addEventListener('mouseup', this.stopResize);
});
if (this.tab.autorun)
this.runQuery(this.query);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
@@ -114,11 +150,12 @@ export default {
query
};
const { status, response } = await Database.rawQuery(params);
const { status, response } = await Schema.rawQuery(params);
if (status === 'success') {
this.results = Array.isArray(response) ? response : [response];
this.resultsCount += this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0);
this.durationsCount += this.results.reduce((acc, curr) => acc + curr.duration, 0);
this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0);
}
else
@@ -137,12 +174,28 @@ export default {
clearTabData () {
this.results = [];
this.resultsCount = 0;
this.durationsCount = 0;
this.affectedCount = 0;
},
resize (e) {
const el = this.$refs.queryEditor.$el;
let editorHeight = e.pageY - el.getBoundingClientRect().top;
if (editorHeight > 400) editorHeight = 400;
if (editorHeight < 50) editorHeight = 50;
this.editorHeight = editorHeight;
},
stopResize () {
window.removeEventListener('mousemove', this.resize);
if (this.$refs.queryTable && this.results.length)
this.$refs.queryTable.resizeResults();
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.resize();
},
onKey (e) {
if (this.isSelected) {
if (this.isSelected && this.isWorkspaceSelected) {
e.stopPropagation();
if (e.key === 'F9')
if (e.key === 'F5')
this.runQuery(this.query);
}
}
@@ -155,11 +208,23 @@ export default {
align-content: baseline;
.workspace-query-runner {
position: relative;
.query-area-resizer {
position: absolute;
height: 5px;
bottom: 40px;
width: 100%;
cursor: ns-resize;
z-index: 99;
}
.workspace-query-runner-footer {
display: flex;
justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem;
align-items: center;
height: 42px;
.workspace-query-buttons {
display: flex;
@@ -181,6 +246,10 @@ export default {
}
}
}
.workspace-query-results {
min-height: 200px;
}
}
</style>

View File

@@ -9,6 +9,7 @@
:context-event="contextEvent"
:selected-rows="selectedRows"
@delete-selected="deleteSelected"
@set-null="setNull"
@close-context="isContext = false"
/>
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
@@ -41,7 +42,7 @@
/>
<span>{{ field.alias || field.name }}</span>
<i
v-if="currentSort === field.name || currentSort === `${field.table}.${field.name}`"
v-if="isSortable && currentSort === field.name || currentSort === `${field.table}.${field.name}`"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
@@ -64,12 +65,11 @@
v-for="row in items"
:key="row._id"
:row="row"
:fields="fields"
:fields="fieldsObj"
:key-usage="keyUsage"
class="tr"
:class="{'selected': selectedRows.includes(row._id)}"
@select-row="selectRow($event, row._id)"
@update-field="updateField($event, getPrimaryValue(row))"
@update-field="updateField($event, row)"
@contextmenu="contextMenu"
/>
</template>
@@ -80,7 +80,8 @@
<script>
import { uidGen } from 'common/libs/uidGen';
import { LONG_TEXT, BLOB } from 'common/fieldTypes';
import arrayToFile from '../libs/arrayToFile';
import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext';
@@ -95,9 +96,9 @@ export default {
},
props: {
results: Array,
tabUid: [String, Number],
connUid: String,
mode: String
mode: String,
isSelected: Boolean
},
data () {
return {
@@ -121,7 +122,15 @@ export default {
return this.getWorkspace(this.connUid).breadcrumbs.schema;
},
primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
const primaryFields = this.fields.filter(field => ['pri', 'uni'].includes(field.key));
if (primaryFields.length > 1 || !primaryFields.length)
return false;
return primaryFields[0];
},
isSortable () {
return this.fields.every(field => field.name);
},
isHardSort () {
return this.mode === 'table' && this.localResults.length === 1000;
@@ -149,6 +158,37 @@ export default {
},
keyUsage () {
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : [];
},
fieldsObj () {
if (this.sortedResults.length) {
const fieldsObj = {};
for (const key in this.sortedResults[0]) {
if (key === '_id') continue;
const fieldObj = this.fields.find(field => {
let fieldNames = [
field.name,
field.alias,
`${field.table}.${field.name}`,
`${field.table}.${field.alias}`,
`${field.tableAlias}.${field.name}`,
`${field.tableAlias}.${field.alias}`
];
if (field.table)
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
if (field.tableAlias)
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
return fieldNames.includes(key);
});
fieldsObj[key] = fieldObj;
}
return fieldsObj;
}
return {};
}
},
watch: {
@@ -195,7 +235,8 @@ export default {
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numLength || field.datePrecision || field.charLength || 0;
else if (TEXT.includes(field.type)) return field.charLength;
return field.length;
},
keyName (key) {
switch (key) {
@@ -238,7 +279,7 @@ export default {
: [];
},
resizeResults () {
if (this.$refs.resultTable) {
if (this.$refs.resultTable && this.isSelected) {
const el = this.$refs.tableWrapper;
if (el) {
@@ -252,37 +293,51 @@ export default {
refreshScroller () {
this.resizeResults();
},
updateField (payload, id) {
if (!this.primaryField)
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') });
else {
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id,
...payload
};
this.$emit('update-field', params);
}
updateField (payload, row) {
const orgRow = this.localResults.find(lr => lr._id === row._id);
delete row._id;
delete orgRow._id;
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(orgRow),
row,
orgRow,
...payload
};
this.$emit('update-field', params);
},
deleteSelected () {
if (!this.primaryField)
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') });
else {
const rowIDs = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row =>
row[this.primaryField.name] ||
row[`${this.primaryField.table}.${this.primaryField.name}`] ||
row[`${this.primaryField.tableAlias}.${this.primaryField.name}`]
);
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
rows: rowIDs
};
this.$emit('delete-selected', params);
}
const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => {
delete row._id;
return row;
});
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
rows
};
this.$emit('delete-selected', params);
},
setNull () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id));
delete row._id;
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(row),
row,
orgRow: row,
field: this.selectedCell.field,
content: null
};
this.$emit('update-field', params);
},
applyUpdate (params) {
const { primary, id, field, table, content } = params;
@@ -324,6 +379,8 @@ export default {
this.selectedRows = [row];
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.selectedCell = cell;
if (!this.selectedRows.includes(cell.id))
this.selectedRows = [cell.id];
@@ -331,6 +388,8 @@ export default {
this.isContext = true;
},
sort (field) {
if (!this.isSortable) return;
if (this.mode === 'query')
field = `${this.getTable(this.resultsetIndex)}.${field}`;
@@ -354,6 +413,20 @@ export default {
},
selectResultset (index) {
this.resultsetIndex = index;
},
downloadTable (format, filename) {
if (!this.sortedResults) return;
const rows = [...this.sortedResults].map(row => {
delete row._id;
return row;
});
arrayToFile({
type: format,
content: rows,
filename
});
}
}
};

View File

@@ -3,8 +3,19 @@
:context-event="contextEvent"
@close-context="closeContext"
>
<div
v-if="selectedRows.length === 1"
class="context-element"
@click="setNull"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-null text-light pr-1" /> {{ $t('message.setNull') }}
</span>
</div>
<div class="context-element" @click="showConfirmModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}</span>
<span class="d-flex">
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</span>
</div>
<ConfirmModal
@@ -60,6 +71,10 @@ export default {
deleteRows () {
this.$emit('delete-selected');
this.closeContext();
},
setNull () {
this.$emit('set-null');
this.closeContext();
}
}
};

View File

@@ -6,17 +6,17 @@
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${getFieldType(cKey)}`"
:class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(getFieldType(cKey), getFieldPrecision(cKey)) | cutText }}</span>
>{{ col | typeFormat(fields[cKey].type.toLowerCase(), fields[cKey].length) | cutText }}</span>
<ForeignKeySelect
v-else-if="foreignKeys.includes(cKey)"
v-else-if="isForeignKey(cKey)"
class="editable-field"
:value.sync="editingContent"
:key-usage="getKeyUsage(cKey)"
@@ -34,6 +34,15 @@
class="editable-field px-2"
@blur="editOFF"
>
<select
v-else-if="inputProps.type === 'boolean'"
v-model="editingContent"
class="form-select small-select editable-field"
@blur="editOFF"
>
<option>true</option>
<option>false</option>
</select>
<input
v-else
ref="editField"
@@ -61,14 +70,52 @@
<div :slot="'body'">
<div class="mb-2">
<div>
<textarea
v-model="editingContent"
class="form-input textarea-editor"
<TextEditor
:value.sync="editingContent"
editor-class="textarea-editor"
:mode="editorMode"
/>
</div>
<div class="editor-field-info">
<div><b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}</div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
<div class="editor-field-info p-vcentered">
<div class="d-flex p-vcentered">
<label for="editorMode" class="form-label mr-2">
<b>{{ $t('word.content') }}</b>:
</label>
<select
id="editorMode"
v-model="editorMode"
class="form-select select-sm"
>
<option value="text">
TEXT
</option>
<option value="html">
HTML
</option>
<option value="xml">
XML
</option>
<option value="json">
JSON
</option>
<option value="svg">
SVG
</option>
<option value="yaml">
YAML
</option>
</select>
</div>
<div class="d-flex">
<div class="p-vcentered">
<div class="mr-4">
<b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}
</div>
<div>
<b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -135,19 +182,21 @@ import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
name: 'WorkspaceQueryTableRow',
components: {
ConfirmModal,
TextEditor,
ForeignKeySelect
},
directives: {
mask
mask: VueMaskDirective
},
filters: {
formatBytes,
@@ -164,6 +213,9 @@ export default {
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
if (DATETIME.includes(type)) {
if (typeof val === 'string')
return val;
let datePrecision = '';
for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
@@ -185,15 +237,18 @@ export default {
return hexToBinary(hex);
}
if (ARRAY.includes(type)) {
if (Array.isArray(val))
return JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
return val;
}
return val;
}
},
props: {
row: Object,
fields: {
type: Array,
default: () => []
},
fields: Object,
keyUsage: Array
},
data () {
@@ -206,6 +261,8 @@ export default {
editingContent: null,
editingType: null,
editingField: null,
editingLength: null,
editorMode: 'text',
contentInfo: {
ext: '',
mime: '',
@@ -219,12 +276,12 @@ export default {
if ([...TEXT, ...LONG_TEXT].includes(this.editingType))
return { type: 'text', mask: false };
if (NUMBER.includes(this.editingType))
if ([...NUMBER, ...FLOAT].includes(this.editingType))
return { type: 'number', mask: false };
if (TIME.includes(this.editingType)) {
let timeMask = '##:##:##';
const precision = this.getFieldPrecision(this.editingField);
const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
@@ -237,7 +294,7 @@ export default {
if (DATETIME.includes(this.editingType)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.getFieldPrecision(this.editingField);
const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
@@ -248,8 +305,8 @@ export default {
if (BLOB.includes(this.editingType))
return { type: 'file', mask: false };
if (BIT.includes(this.editingType))
return { type: 'text', mask: false };
if (BOOLEAN.includes(this.editingType))
return { type: 'boolean', mask: false };
return { type: 'text', mask: false };
},
@@ -260,80 +317,70 @@ export default {
return this.keyUsage.map(key => key.field);
},
isEditable () {
return this.fields ? !!(this.fields[0].schema && this.fields[0].table) : false;
if (this.fields) {
const nElements = Object.keys(this.fields).reduce((acc, curr) => {
acc.add(this.fields[curr].table);
acc.add(this.fields[curr].schema);
return acc;
}, new Set([]));
if (nElements.size > 2) return false;
return !!(this.fields[Object.keys(this.fields)[0]].schema && this.fields[Object.keys(this.fields)[0]].table);
}
return false;
}
},
watch: {
fields () {
this.fields.forEach(field => {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
},
methods: {
getFieldType (cKey) {
let type = 'unknown';
const field = this.getFieldObj(cKey);
if (field)
type = field.type;
isForeignKey (key) {
if (key.includes('.'))
key = key.split('.').pop();
return type.toLowerCase();
},
getFieldPrecision (cKey) {
let length = 0;
const field = this.getFieldObj(cKey);
if (field)
length = field.datePrecision;
return length;
},
getFieldObj (cKey) {
return this.fields.filter(field => {
let fieldNames = [
field.name,
field.alias,
`${field.table}.${field.name}`,
`${field.table}.${field.alias}`,
`${field.tableAlias}.${field.name}`,
`${field.tableAlias}.${field.alias}`
];
if (field.table)
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
if (field.tableAlias)
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
return fieldNames.includes(cKey);
})[0];
return this.foreignKeys.includes(key);
},
isNull (value) {
return value === null ? ' is-null' : '';
},
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
bufferToBase64 (val) {
return bufferToBase64(val);
},
editON (event, content, field) {
if (!this.isEditable) return;
const type = this.getFieldType(field).toUpperCase(); ;
this.originalContent = content;
window.addEventListener('keydown', this.onKey);
const type = this.fields[field].type.toUpperCase(); ;
this.originalContent = this.$options.filters.typeFormat(content, type, this.fields[field].length);
this.editingType = type;
this.editingField = field;
this.editingLength = this.fields[field].length;
if (LONG_TEXT.includes(type)) {
if ([...LONG_TEXT, ...ARRAY, ...TEXT_SEARCH].includes(type)) {
this.isTextareaEditor = true;
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type);
this.editingContent = this.$options.filters.typeFormat(content, type);
return;
}
if (BLOB.includes(type)) {
this.isBlobEditor = true;
this.editingContent = this.originalContent || '';
this.editingContent = content || '';
this.fileToUpload = null;
this.willBeDeleted = false;
if (this.originalContent !== null) {
if (content !== null) {
const buff = Buffer.from(this.editingContent);
if (buff.length) {
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
@@ -349,7 +396,7 @@ export default {
}
// Inline editable fields
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.getFieldPrecision(field));
this.editingContent = this.originalContent;
this.$nextTick(() => { // Focus on input
event.target.blur();
@@ -360,10 +407,18 @@ export default {
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
},
editOFF () {
if (!this.editingField) return;
this.isInlineEditor[this.editingField] = false;
let content;
if (!BLOB.includes(this.editingType)) {
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType)) return;// If not changed
if ([...DATETIME, ...TIME].includes(this.editingType)) {
if (this.editingContent.substring(this.editingContent.length - 1) === '.')
this.editingContent = this.editingContent.slice(0, -1);
}
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType, this.editingLength)) return;// If not changed
content = this.editingContent;
}
else { // Handle file upload
@@ -378,13 +433,14 @@ export default {
}
this.$emit('update-field', {
field: this.getFieldObj(this.editingField).name,
field: this.fields[this.editingField].name,
type: this.editingType,
content
});
this.editingType = null;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
},
hideEditorModal () {
this.isTextareaEditor = false;
@@ -416,14 +472,27 @@ export default {
};
this.willBeDeleted = true;
},
contextMenu (event, cell) {
this.$emit('update-field', event, cell);
},
selectRow (event, row) {
this.$emit('select-row', event, row);
},
getKeyUsage (keyName) {
if (keyName.includes('.'))
return this.keyUsage.find(key => key.field === keyName.split('.').pop());
return this.keyUsage.find(key => key.field === keyName);
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
}
}
};
@@ -453,7 +522,7 @@ export default {
}
.editor-field-info {
margin-top: 0.6rem;
margin-top: 0.4rem;
display: flex;
justify-content: space-between;
white-space: normal;

View File

@@ -32,15 +32,45 @@
</div>
</div>
</div>
<button
v-if="isTable"
class="btn btn-dark btn-sm"
@click="showAddModal"
:disabled="isQuering"
@click="showFakerModal"
>
<span>{{ $t('word.add') }}</span>
<span>{{ $t('message.tableFiller') }}</span>
<i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button>
<div class="dropdown export-dropdown pr-2">
<button
:disabled="isQuering"
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
tabindex="0"
>
<span>{{ $t('word.export') }}</span>
<i class="mdi mdi-24px mdi-file-export ml-1" />
<i class="mdi mdi-24px mdi-menu-down" />
</button>
<ul class="menu text-left">
<li class="menu-item">
<a class="c-hand" @click="downloadTable('json')">JSON</a>
</li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li>
</ul>
</div>
</div>
<div class="workspace-query-info">
<div
v-if="results.length"
class="d-flex"
:title="$t('message.queryDuration')"
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ results[0].duration / 1000 }}s</b>
</div>
<div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
</div>
@@ -53,13 +83,15 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12">
<div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" />
<WorkspaceQueryTable
v-if="results"
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:conn-uid="connection.uid"
:is-selected="isSelected"
mode="table"
@update-field="updateField"
@delete-selected="deleteSelected"
@@ -74,21 +106,33 @@
@hide="hideAddModal"
@reload="reloadTable"
/>
<ModalFakerRows
v-if="isFakerModal"
:fields="fields"
:key-usage="keyUsage"
:tab-uid="tabUid"
@hide="hideFakerModal"
@reload="reloadTable"
/>
</div>
</template>
<script>
import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import ModalNewTableRow from '@/components/ModalNewTableRow';
import ModalFakerRows from '@/components/ModalFakerRows';
import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs';
export default {
name: 'WorkspaceTableTab',
components: {
BaseLoader,
WorkspaceQueryTable,
ModalNewTableRow
ModalNewTableRow,
ModalFakerRows
},
mixins: [tableTabs],
props: {
@@ -102,6 +146,7 @@ export default {
results: [],
lastTable: null,
isAddModal: false,
isFakerModal: false,
autorefreshTimer: 0,
refreshInterval: null,
sortParams: {}
@@ -109,13 +154,17 @@ export default {
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'data';
return this.workspace.selected_tab === 'data' && this.workspace.uid === this.selectedWorkspace;
},
isTable () {
return !!this.workspace.breadcrumbs.table;
},
fields () {
return this.results.length ? this.results[0].fields : [];
@@ -205,6 +254,13 @@ export default {
hideAddModal () {
this.isAddModal = false;
},
showFakerModal () {
if (this.isQuering) return;
this.isFakerModal = true;
},
hideFakerModal () {
this.isFakerModal = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
@@ -222,7 +278,21 @@ export default {
this.reloadTable();
}, this.autorefreshTimer * 1000);
}
},
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, this.table);
}
}
};
</script>
<style lang="scss" scoped>
.export-dropdown {
.menu {
min-width: 100%;
.menu-item a:hover {
background: $bg-color-gray;
}
}
}
</style>

View File

@@ -69,19 +69,5 @@ module.exports = {
uploadFile: 'رفع ملف',
addNewRow: 'إضافة صف جديد',
numberOfInserts: 'عدد الإدراجات'
},
// Date and Time
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric'
}
};

View File

@@ -78,7 +78,30 @@ module.exports = {
parameters: 'Parameters',
function: 'Function | Functions',
deterministic: 'Deterministic',
context: 'Context'
context: 'Context',
export: 'Export',
returns: 'Returns',
timing: 'Timing',
state: 'State',
execution: 'Execution',
starts: 'Starts',
ends: 'Ends',
ssl: 'SSL',
privateKey: 'Private key',
certificate: 'Certificate',
caCertificate: 'CA certificate',
ciphers: 'Ciphers',
upload: 'Upload',
browse: 'Browse',
faker: 'Faker',
content: 'Content',
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
tools: 'Tools',
variables: 'Variables',
processes: 'Processes',
database: 'Database'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -158,20 +181,193 @@ module.exports = {
thereAreNoParameters: 'There are no parameters',
createNewParameter: 'Create new parameter',
createNewRoutine: 'Create new stored routine',
deleteRoutine: 'Delete stored routine'
deleteRoutine: 'Delete stored routine',
functionBody: 'Function body',
createNewFunction: 'Create new function',
deleteFunction: 'Delete function',
schedulerBody: 'Scheduler body',
createNewScheduler: 'Create new scheduler',
deleteScheduler: 'Delete scheduler',
preserveOnCompletion: 'Preserve on completion',
enableSsl: 'Enable SSL',
manualValue: 'Manual value',
tableFiller: 'Table Filler',
fakeDataLanguage: 'Fake data language',
searchForElements: 'Search for elements',
selectAll: 'Select all',
queryDuration: 'Query duration',
includeBetaUpdates: 'Include beta updates',
setNull: 'Set NULL',
processesList: 'Processes list',
processInfo: 'Process info',
manageUsers: 'Manage users',
createNewSchema: 'Create new schema',
schemaName: 'Schema name',
editSchema: 'Edit schema',
deleteSchema: 'Delete schema'
},
// Date and Time
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric'
faker: {
address: 'Address',
commerce: 'Commerce',
company: 'Company',
database: 'Database',
date: 'Date',
finance: 'Finance',
git: 'Git',
hacker: 'Hacker',
internet: 'Internet',
lorem: 'Lorem',
name: 'Name',
music: 'Music',
phone: 'Phone',
random: 'Random',
system: 'System',
time: 'Time',
vehicle: 'Vehicle',
zipCode: 'Zip code',
zipCodeByState: 'Zip code by state',
city: 'City',
cityPrefix: 'City prefix',
citySuffix: 'City suffix',
streetName: 'Street name',
streetAddress: 'Street address',
streetSuffix: 'Street suffix',
streetPrefix: 'Street prefix',
secondaryAddress: 'Secondary address',
county: 'County',
country: 'Country',
countryCode: 'Country code',
state: 'State',
stateAbbr: 'State abbreviation',
latitude: 'Latitude',
longitude: 'Longitude',
direction: 'Direction',
cardinalDirection: 'Cardinal direction',
ordinalDirection: 'Ordinal direction',
nearbyGPSCoordinate: 'Nearby GPS coordinate',
timeZone: 'Time zone',
color: 'Color',
department: 'Department',
productName: 'Product name',
price: 'Price',
productAdjective: 'Product adjective',
productMaterial: 'Product material',
product: 'Product',
productDescription: 'Product description',
suffixes: 'Suffixes',
companyName: 'Company name',
companySuffix: 'Company suffix',
catchPhrase: 'Catch phrase',
bs: 'BS',
catchPhraseAdjective: 'Catch phrase adjective',
catchPhraseDescriptor: 'Catch phrase descriptor',
catchPhraseNoun: 'Catch phrase noun',
bsAdjective: 'BS adjective',
bsBuzz: 'BS buzz',
bsNoun: 'BS noun',
column: 'Column',
type: 'Type',
collation: 'Collation',
engine: 'Engine',
past: 'Past',
future: 'Future',
between: 'Between',
recent: 'Recent',
soon: 'Soon',
month: 'Month',
weekday: 'Weekday',
account: 'Account',
accountName: 'Account name',
routingNumber: 'Routing number',
mask: 'Mask',
amount: 'Amount',
transactionType: 'Transaction type',
currencyCode: 'Currency code',
currencyName: 'Currency name',
currencySymbol: 'Currency symbol',
bitcoinAddress: 'Bitcoin address',
litecoinAddress: 'Litecoin address',
creditCardNumber: 'Credit card number',
creditCardCVV: 'Credit card CVV',
ethereumAddress: 'Ethereum address',
iban: 'Iban',
bic: 'Bic',
transactionDescription: 'Transaction description',
branch: 'Branch',
commitEntry: 'Commit entry',
commitMessage: 'Commit message',
commitSha: 'Commit SHA',
shortSha: 'Short SHA',
abbreviation: 'Abbreviation',
adjective: 'Adjective',
noun: 'Noun',
verb: 'Verb',
ingverb: 'Ingverb',
phrase: 'Phrase',
avatar: 'Avatar',
email: 'Email',
exampleEmail: 'Example email',
userName: 'Username',
protocol: 'Protocol',
url: 'Url',
domainName: 'Domin name',
domainSuffix: 'Domain suffix',
domainWord: 'Domain word',
ip: 'Ip',
ipv6: 'Ipv6',
userAgent: 'User agent',
mac: 'Mac',
password: 'Password',
word: 'Word',
words: 'Words',
sentence: 'Sentence',
slug: 'Slug',
sentences: 'Sentences',
paragraph: 'Paragraph',
paragraphs: 'Paragraphs',
text: 'Text',
lines: 'Lines',
genre: 'Genre',
firstName: 'First name',
lastName: 'Last name',
middleName: 'Middle name',
findName: 'Full name',
jobTitle: 'Job title',
gender: 'Gender',
prefix: 'Prefix',
suffix: 'Suffix',
title: 'Title',
jobDescriptor: 'Job descriptor',
jobArea: 'Job area',
jobType: 'Job type',
phoneNumber: 'Phone number',
phoneNumberFormat: 'Phone number format',
phoneFormats: 'Phone formats',
number: 'Number',
float: 'Float',
arrayElement: 'Array element',
arrayElements: 'Array elements',
objectElement: 'Object element',
uuid: 'Uuid',
boolean: 'Boolean',
image: 'Image',
locale: 'Locale',
alpha: 'Alpha',
alphaNumeric: 'Alphanumeric',
hexaDecimal: 'Hexadecimal',
fileName: 'File name',
commonFileName: 'Common file name',
mimeType: 'Mime type',
commonFileType: 'Common file type',
commonFileExt: 'Common file extension',
fileType: 'File type',
fileExt: 'File extension',
directoryPath: 'Directory path',
filePath: 'File path',
semver: 'Semver',
manufacturer: 'Manufacturer',
model: 'Model',
fuel: 'Fuel',
vin: 'Vin'
}
};

View File

@@ -72,19 +72,5 @@ module.exports = {
numberOfInserts: 'Numero de inserciones',
openNewTab: 'Abrir nueva pestaña',
affectedRows: 'Filas afectadas'
},
// Date and Time
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric'
}
};

352
src/renderer/i18n/fr-FR.js Normal file
View File

@@ -0,0 +1,352 @@
module.exports = {
word: {
edit: 'Éditer',
save: 'Enregistrer',
close: 'Fermer',
delete: 'Supprimer',
confirm: 'Confirmer',
cancel: 'Annuler',
send: 'Envoyer',
connectionName: 'Nom de connexion',
client: 'Client',
hostName: 'Nom d\'hôte',
port: 'Port',
user: 'Utilisateur',
password: 'Mot de passe',
credentials: 'Identifiants',
connect: 'Se connecter',
connected: 'Connecté',
disconnect: 'Se déconnecter',
disconnected: 'Déconnecté',
refresh: 'Rafraichir',
settings: 'Paramètres',
general: 'Général',
themes: 'Thèmes',
update: 'Mise à jour',
about: 'À propos',
language: 'Langue',
version: 'Version',
donate: 'Faire un don',
run: 'Exécuter',
schema: 'Schéma',
results: 'Résutats',
size: 'Taille',
seconds: 'Secondes',
type: 'Type',
mimeType: 'Mime-Type',
download: 'Télécharger',
add: 'Ajouter',
data: 'Données',
properties: 'Propriétés',
insert: 'Insérer',
connecting: 'Connexion',
name: 'Nom',
collation: 'Collation',
clear: 'Effacer',
options: 'Options',
autoRefresh: 'Auto-rafraichissement',
indexes: 'Index',
foreignKeys: 'Clés étrangères',
length: 'Taille',
unsigned: 'Non-signé',
default: 'Défaut',
comment: 'Commentaire',
key: 'Clé | Clés',
order: 'Ordre',
expression: 'Expression',
autoIncrement: 'Auto Increment',
engine: 'Engine',
field: 'Champ | Champs',
approximately: 'Approximativement',
total: 'Totale',
table: 'Table',
discard: 'Abandonner',
stay: 'Rester',
author: 'Auteur',
light: 'Clair',
dark: 'Sombre',
autoCompletion: 'Completion auto',
application: 'Application',
editor: 'Editeur',
view: 'Vue',
definer: 'Définisseur',
algorithm: 'Algorithme',
trigger: 'Déclencheur | Déclencheurs',
storedRoutine: 'Procedure stockée | Procedures stockées',
scheduler: 'Opération planifiée | Opérations planifiées',
event: 'Evenement',
parameters: 'Paramètres',
function: 'Fonction | Fonctions',
deterministic: 'Déterministe',
context: 'Contextz',
export: 'Exporter',
returns: 'Retourner',
timing: 'Horaire',
state: 'État',
execution: 'Exécution',
starts: 'Débuts',
ends: 'Fins',
ssl: 'SSL',
privateKey: 'Clé privée',
certificate: 'Certificat',
caCertificate: 'CA certificat',
ciphers: 'Chiffrement',
upload: 'Charger',
browse: 'Parcourir',
faker: 'Faker'
},
message: {
appWelcome: 'Bienvenu sur le client SQL Antares!',
appFirstStep: 'Première étape: Créer une nouvelle connexion à une base de données.',
addConnection: 'Ajouter une connexion',
createConnection: 'Créer une connexion',
createNewConnection: 'Créer une nouvelle connexion',
askCredentials: 'Demander les identifiants',
testConnection: 'Tester la connexion',
editConnection: 'Editer la connexion',
deleteConnection: 'Supprimer la connexion',
deleteCorfirm: 'Êtes-vous sûr de vouloir annuler',
connectionSuccessfullyMade: 'Connexion établie avec succès!',
madeWithJS: 'Créé avec 💛 et JavaScript!',
checkForUpdates: 'Rechercher des mises à jour',
noUpdatesAvailable: 'Aucune mise à jour disponible',
checkingForUpdate: 'Recherche de mise à jour',
checkFailure: 'Erreur lors de la recherche, essayez plus tard',
updateAvailable: 'Une mise à jour est disponible',
downloadingUpdate: 'Téléchargement de la mise à jour',
updateDownloaded: 'Mise à jour téléchargée',
restartToInstall: 'Redémarrer Antares pour l\'installer',
unableEditFieldWithoutPrimary: 'Impossible de modifier un champ sans clé primaire dans l\'ensemble de résultats',
editCell: 'Modifier une cellule',
deleteRows: 'Supprimer une ligne | Supprimer {count} lignes',
confirmToDeleteRows: 'Êtes-vous sûr de vouloir supprimer une ligne? | Êtes-vous sûr de vouloir supprimer {count} lignes?',
notificationsTimeout: 'Délai d\'expiration des notifications',
uploadFile: 'Charger un fichier',
addNewRow: 'Ajouter une ligne',
numberOfInserts: 'Nombre d\'insertions',
openNewTab: 'Ouvrir un nouvel onglet',
affectedRows: 'Lignes concernées',
createNewDatabase: 'Créer une nouvelle base de données',
databaseName: 'Nom par défaut',
serverDefault: 'Serveur par défaut',
deleteDatabase: 'Supprimer la base de données',
editDatabase: 'Modifier la base de données',
clearChanges: 'Effacer les modifications',
addNewField: 'Ajouter un champ',
manageIndexes: 'Gérer les index',
manageForeignKeys: 'Gérer les clés étrangères',
allowNull: 'NULL autorisé',
zeroFill: 'Remplissage zéro',
customValue: 'Valeur personnalisée',
onUpdate: 'Lors d\'une mise à jour',
deleteField: 'Supprimer le champ',
createNewIndex: 'Créer un index',
addToIndex: 'Ajouter à l\'index',
createNewTable: 'Créer une nouvelle table',
emptyTable: 'Table vide',
deleteTable: 'Supprimer la table',
emptyCorfirm: 'Êtes-vous sûr de vouloir videz',
unsavedChanges: 'Changements non sauvegardés',
discardUnsavedChanges: 'Vous avez des modifications non sauvegardées. En quittant cet onglet, ces changements seront supprimés.',
thereAreNoIndexes: 'Il n\'y a pas d\'indexes',
thereAreNoForeign: 'Il n\'y a pas de clés étrangères',
createNewForeign: 'Créer une clés étrangère',
referenceTable: 'Table de référence',
referenceField: 'CHamp de référence',
foreignFields: 'Champ étrangé',
invalidDefault: 'Valeur par défaut invalide',
onDelete: 'Lors de la suppression',
applicationTheme: 'Thème de l\'application',
editorTheme: 'Thème de l\'éditeur',
wrapLongLines: 'Retour à la ligne automatique',
selectStatement: 'Sélectionnez la déclaration',
triggerStatement: 'Déclaration de déclencheur',
sqlSecurity: 'Sécurité SQL',
updateOption: 'Options de mises à jour',
deleteView: 'Supprimer la vue',
createNewView: 'Créer une nouvelle vue',
deleteTrigger: 'Supprimer le déclencheur',
createNewTrigger: 'Créer un nouveau déclencheur',
currentUser: 'Utilisateur actuel',
routineBody: 'Contenu de la procédure',
dataAccess: 'Accès aux données',
thereAreNoParameters: 'Il n\'y a pas de paramètres',
createNewParameter: 'Créer un nouveau paramètre',
createNewRoutine: 'Créer une nouvelle procédure stockée',
deleteRoutine: 'Supprimer une procédure stockée',
functionBody: 'Contenu de la fonction',
createNewFunction: 'Créer une nouvelle fonction',
deleteFunction: 'Supprimer la fonction',
schedulerBody: 'Contenu du opération planifiée',
createNewScheduler: 'Créere une nouvelle opération planifiée',
deleteScheduler: 'Supprimer l\'opération planifiée',
preserveOnCompletion: 'Préserver à l\'achèvement',
enableSsl: 'Activer le SSL',
manualValue: 'Valeur manuelle',
tableFiller: 'Remplisseur de table'
},
faker: {
address: 'Adresse',
commerce: 'Commerce',
company: 'Entreprise',
database: 'Base de données',
date: 'Date',
finance: 'Finance',
git: 'Git',
hacker: 'Hacker',
internet: 'Internet',
lorem: 'Lorem',
name: 'Nom',
music: 'Musique',
phone: 'Téléphone',
random: 'Aléatoire',
system: 'Système',
time: 'Temps',
vehicle: 'Véhicle',
zipCode: 'Code postal',
zipCodeByState: 'Code postal par région',
city: 'Ville',
cityPrefix: 'Préfixe de la ville',
citySuffix: 'Suffixe de la ville',
streetName: 'Ne de la rue',
streetAddress: 'Adresse',
streetSuffix: 'Suffixe de la rue',
streetPrefix: 'Préfixe de la rue',
secondaryAddress: 'Adresse secondaire',
county: 'Comté',
country: 'Pays',
countryCode: 'Code du pays',
state: 'Région',
stateAbbr: 'Abbreviation de la région',
latitude: 'Latitude',
longitude: 'Longitude',
direction: 'Direction',
cardinalDirection: 'Orientation cardinale',
ordinalDirection: 'Orientation originale',
nearbyGPSCoordinate: 'Coordonnées GPS des environs',
timeZone: 'Fuseau horaire',
color: 'Couleur',
department: 'Département',
productName: 'Nom de produit',
price: 'Prix',
productAdjective: 'Adjectif du produit',
productMaterial: 'Matériau du produit',
product: 'Produit',
productDescription: 'Description du produit',
suffixes: 'Suffixes',
companyName: 'Nom de l\'entreprise',
companySuffix: 'Suffixe de l\'entreprise',
catchPhrase: 'Slogan',
bs: 'BS',
catchPhraseAdjective: 'Adjectif du slogan',
catchPhraseDescriptor: 'Descripteur de slogan',
catchPhraseNoun: 'Nom de la phrase d\'accroche',
bsAdjective: 'Adjectif BS',
bsBuzz: 'BS buzz',
bsNoun: 'Nom BS',
column: 'Colonne',
type: 'Type',
collation: 'Collation',
engine: 'Engine',
past: 'Passé',
future: 'Futur',
between: 'Entre',
recent: 'Récent',
soon: 'Bientôt',
month: 'Mois',
weekday: 'Mercredi',
account: 'Compte',
accountName: 'Nom de compte',
routingNumber: 'Numéros de routage',
mask: 'Masque',
amount: 'Quantité',
transactionType: 'Type de transaction',
currencyCode: 'Code de la devise',
currencyName: 'Nom de la devise',
currencySymbol: 'Symbole de la devise',
bitcoinAddress: 'Adresse Bitcoin',
litecoinAddress: 'Adresse Litecoin',
creditCardNumber: 'Numero de carte de crédit',
creditCardCVV: 'Cryptogramme',
ethereumAddress: 'Adresse Ethereum',
iban: 'Iban',
bic: 'Bic',
transactionDescription: 'Description de la transaction',
branch: 'Branche',
commitEntry: 'Valider l\'entrée',
commitMessage: 'Valider le message',
commitSha: 'Valider le SHA',
shortSha: 'SHA court',
abbreviation: 'Abbréviation',
adjective: 'Adjectif',
noun: 'Nom',
verb: 'Verbe',
ingverb: 'Ingverb',
phrase: 'Phrase',
avatar: 'Avatar',
email: 'Email',
exampleEmail: 'Exemple d\'email',
userName: 'Nom d\'utilisateur',
protocol: 'Protocole',
url: 'Url',
domainName: 'Nom de domaine',
domainSuffix: 'Suffixe du nom de domaine',
domainWord: 'Mot de domaine',
ip: 'Ip',
ipv6: 'Ipv6',
userAgent: 'User agent',
mac: 'Mac',
password: 'Mot de passe',
word: 'Mot',
words: 'Mots',
sentence: 'Phrase',
slug: 'Slug',
sentences: 'Phrases',
paragraph: 'Paragraphe',
paragraphs: 'Paragraphes',
text: 'Texte',
lines: 'Lignes',
genre: 'Genre',
firstName: 'Prénom',
lastName: 'Nom',
middleName: 'Deuxième prénom',
findName: 'Nom et prénom',
jobTitle: 'Intitulé du poste',
gender: 'Genre',
prefix: 'Préfixe',
suffix: 'Suffixe',
title: 'Titre',
jobDescriptor: 'Descripteur de poste',
jobArea: 'Domaine d\'activité',
jobType: 'Type de poste',
phoneNumber: 'Numéro de téléphone',
phoneNumberFormat: 'Format du numéro de téléphone',
phoneFormats: 'Formats de téléphone',
number: 'Numéro',
float: 'Nombre décimaux',
arrayElement: 'Élément de Liste',
arrayElements: 'Éléments de liste',
objectElement: 'Élément d\'objet',
uuid: 'Uuid',
boolean: 'Boolean',
image: 'Image',
locale: 'Locale',
alpha: 'Alpha',
alphaNumeric: 'Alphanumerique',
hexaDecimal: 'Hexadecimale',
fileName: 'Nom deu fichier',
commonFileName: 'Nom de fichier commun',
mimeType: 'Mime type',
commonFileType: 'Type de dossier commun',
commonFileExt: 'Extension de fichier commun',
fileType: 'Type de fichier',
fileExt: 'Extension de fichier',
directoryPath: 'Chemin du répertoire',
filePath: 'Chemin d\'accès au fichier',
semver: 'Semver',
manufacturer: 'Fabricant',
model: 'Modèle',
fuel: 'Carburant',
vin: 'Vin'
}
};

View File

@@ -8,7 +8,8 @@ const i18n = new VueI18n({
'en-US': require('./en-US'),
'it-IT': require('./it-IT'),
'ar-SA': require('./ar-SA'),
'es-ES': require('./es-ES')
'es-ES': require('./es-ES'),
'fr-FR': require('./fr-FR')
}
});
export default i18n;

View File

@@ -72,19 +72,5 @@ module.exports = {
numberOfInserts: 'Numero di insert',
openNewTab: 'Apri nuova scheda',
affectedRows: 'Righe interessate'
},
// Date and Time
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric'
}
};

View File

@@ -2,5 +2,6 @@ export default {
'en-US': 'English',
'it-IT': 'Italiano',
'ar-SA': 'العربية',
'es-ES': 'Español'
'es-ES': 'Español',
'fr-FR': 'Français'
};

View File

@@ -1,44 +0,0 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static createDatabase (params) {
return ipcRenderer.invoke('create-database', params);
}
static updateDatabase (params) {
return ipcRenderer.invoke('update-database', params);
}
static getDatabaseCollation (params) {
return ipcRenderer.invoke('get-database-collation', params);
}
static deleteDatabase (params) {
return ipcRenderer.invoke('delete-database', params);
}
static getStructure (uid) {
return ipcRenderer.invoke('get-structure', uid);
}
static getCollations (uid) {
return ipcRenderer.invoke('get-collations', uid);
}
static getVariables (uid) {
return ipcRenderer.invoke('get-variables', uid);
}
static getEngines (uid) {
return ipcRenderer.invoke('get-engines', uid);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}
static rawQuery (params) {
return ipcRenderer.invoke('raw-query', params);
}
}

View File

@@ -0,0 +1,20 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getFunctionInformations (params) {
return ipcRenderer.invoke('get-function-informations', params);
}
static dropFunction (params) {
return ipcRenderer.invoke('drop-function', params);
}
static alterFunction (params) {
return ipcRenderer.invoke('alter-function', params);
}
static createFunction (params) {
return ipcRenderer.invoke('create-function', params);
}
}

View File

@@ -0,0 +1,20 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getSchedulerInformations (params) {
return ipcRenderer.invoke('get-scheduler-informations', params);
}
static dropScheduler (params) {
return ipcRenderer.invoke('drop-scheduler', params);
}
static alterScheduler (params) {
return ipcRenderer.invoke('alter-scheduler', params);
}
static createScheduler (params) {
return ipcRenderer.invoke('create-scheduler', params);
}
}

View File

@@ -0,0 +1,52 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static createSchema (params) {
return ipcRenderer.invoke('create-schema', params);
}
static updateSchema (params) {
return ipcRenderer.invoke('update-schema', params);
}
static getDatabaseCollation (params) {
return ipcRenderer.invoke('get-schema-collation', params);
}
static deleteSchema (params) {
return ipcRenderer.invoke('delete-schema', params);
}
static getStructure (params) {
return ipcRenderer.invoke('get-structure', params);
}
static getCollations (uid) {
return ipcRenderer.invoke('get-collations', uid);
}
static getVariables (uid) {
return ipcRenderer.invoke('get-variables', uid);
}
static getEngines (uid) {
return ipcRenderer.invoke('get-engines', uid);
}
static getVersion (uid) {
return ipcRenderer.invoke('get-version', uid);
}
static getProcesses (uid) {
return ipcRenderer.invoke('get-processes', uid);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}
static rawQuery (params) {
return ipcRenderer.invoke('raw-query', params);
}
}

View File

@@ -30,6 +30,10 @@ export default class {
return ipcRenderer.invoke('insert-table-rows', params);
}
static insertTableFakeRows (params) {
return ipcRenderer.invoke('insert-table-fake-rows', params);
}
static getForeignList (params) {
return ipcRenderer.invoke('get-foreign-list', params);
}

View File

@@ -0,0 +1,37 @@
const arrayToFile = args => {
let mime;
let content;
switch (args.type) {
case 'csv': {
mime = 'text/csv';
const csv = [];
if (args.content.length)
csv.push(Object.keys(args.content[0]).join(';'));
for (const row of args.content)
csv.push(Object.values(row).map(col => typeof col === 'string' ? `"${col}"` : col).join(';'));
content = csv.join('\n');
break;
}
case 'json':
mime = 'application/json';
content = JSON.stringify(args.content, null, 3);
break;
default:
break;
}
const file = new Blob([content], { type: mime });
const downloadLink = document.createElement('a');
downloadLink.download = `${args.filename}.${args.type}`;
downloadLink.href = window.URL.createObjectURL(file);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
};
export default arrayToFile;

View File

@@ -1239,6 +1239,18 @@ ace.define('ace/autocomplete/popup', ['require', 'exports', 'module', 'ace/virtu
case 'view':
iconClass = 'mdi-table-eye';
break;
case 'trigger':
iconClass = 'mdi-table-cog';
break;
case 'routine':
iconClass = 'mdi-sync-circle';
break;
case 'function':
iconClass = 'mdi-arrow-right-bold-box';
break;
case 'scheduler':
iconClass = 'mdi-calendar-clock';
break;
case 'keyword':
iconClass = 'mdi-cube';
break;

View File

@@ -22,6 +22,14 @@
"mediumtext": $string-color,
"longtext": $string-color,
"json": $string-color,
"name": $string-color,
"character": $string-color,
"character_varying": $string-color,
"cidr": $string-color,
"inet": $string-color,
"macaddr": $string-color,
"macaddr8": $string-color,
"uuid": $string-color,
"int": $number-color,
"tinyint": $number-color,
"smallint": $number-color,
@@ -30,20 +38,44 @@
"double": $number-color,
"decimal": $number-color,
"bigint": $number-color,
"newdecimal": $number-color,
"integer": $number-color,
"numeric": $number-color,
"smallserial": $number-color,
"serial": $number-color,
"bigserial": $number-color,
"real": $number-color,
"double_precision": $number-color,
"oid": $number-color,
"xid": $number-color,
"money": $number-color,
"datetime": $date-color,
"date": $date-color,
"time": $date-color,
"time_with_time_zone": $date-color,
"year": $date-color,
"timestamp": $date-color,
"timestamp_without_time_zone": $date-color,
"timestamp_with_time_zone": $date-color,
"bit": $bit-color,
"bit_varying": $bit-color,
"binary": $blob-color,
"varbinary": $blob-color,
"blob": $blob-color,
"tinyblob": $blob-color,
"mediumblob": $blob-color,
"medium_blob": $blob-color,
"longblob": $blob-color,
"bytea": $blob-color,
"enum": $enum-color,
"set": $enum-color,
"boolean": $enum-color,
"interval": $array-color,
"array": $array-color,
"anyarray": $array-color,
"tsvector": $array-color,
"tsquery": $array-color,
"pg_node_tree": $array-color,
"unknown": $unknown-color,
)
);

View File

@@ -15,7 +15,8 @@
}
&.key-mul,
&.key-INDEX {
&.key-INDEX,
&.key-KEY {
color: palegreen;
}

View File

@@ -14,6 +14,7 @@ $number-color: cornflowerblue;
$date-color: coral;
$bit-color: lightskyblue;
$blob-color: darkorchid;
$array-color: greenyellow;
$enum-color: gold;
$unknown-color: gray;

View File

@@ -96,6 +96,10 @@ body {
}
}
.process-row .td:last-child {
width: 100%;
}
// Scrollbars
::-webkit-scrollbar {
width: 10px;
@@ -210,6 +214,10 @@ body {
border-color: $primary-color;
}
.form-input[type="file"] {
overflow: hidden;
}
.input-group .input-group-addon {
border-color: #3f3f3f;
z-index: 1;

View File

@@ -2,7 +2,6 @@
import Vue from 'vue';
import Vuex from 'vuex';
import VuexPersist from 'vuex-persist';
import application from './modules/application.store';
import settings from './modules/settings.store';
@@ -12,15 +11,6 @@ import notifications from './modules/notifications.store';
import ipcUpdates from './plugins/ipcUpdates';
const vuexLocalStorage = new VuexPersist({
key: 'application', // The key to store the state on in the storage provider.
storage: window.localStorage,
reducer: state => ({
connections: state.connections,
settings: state.settings
})
});
Vue.use(Vuex);
export default new Vuex.Store({
@@ -33,7 +23,6 @@ export default new Vuex.Store({
notifications
},
plugins: [
vuexLocalStorage.plugin,
ipcUpdates
]
});

View File

@@ -11,12 +11,14 @@ export default {
selected_setting_tab: 'general',
selected_conection: {},
update_status: 'noupdate', // noupdate, available, checking, nocheck, downloading, downloaded
download_progress: 0
download_progress: 0,
base_completer: [] // Needed to reset ace editor, due global-only ace completer
},
getters: {
isLoading: state => state.is_loading,
appName: state => state.app_name,
appVersion: state => state.app_version,
getBaseCompleter: state => state.base_completer,
getSelectedConnection: state => state.selected_conection,
isNewModal: state => state.is_new_modal,
isSettingModal: state => state.is_setting_modal,
@@ -28,6 +30,9 @@ export default {
SET_LOADING_STATUS (state, payload) {
state.is_loading = payload;
},
SET_BASE_COMPLETER (state, payload) {
state.base_completer = payload;
},
SHOW_NEW_CONNECTION_MODAL (state) {
state.is_new_modal = true;
},
@@ -52,6 +57,9 @@ export default {
setLoadingStatus ({ commit }, payload) {
commit('SET_LOADING_STATUS', payload);
},
setBaseCompleter ({ commit }, payload) {
commit('SET_BASE_COMPLETER', payload);
},
// Modals
showNewConnModal ({ commit }) {
commit('SHOW_NEW_CONNECTION_MODAL');

View File

@@ -1,7 +1,13 @@
'use strict';
import Store from 'electron-store';
import crypto from 'crypto';
import Application from '../../ipc-api/Application';
const key = Application.getKey();
const key = Application.getKey() || localStorage.getItem('key');
if (!key)
localStorage.setItem('key', crypto.randomBytes(16).toString('hex'));
else
localStorage.setItem('key', key);
const persistentStore = new Store({
name: 'connections',
@@ -12,7 +18,7 @@ export default {
namespaced: true,
strict: true,
state: {
connections: persistentStore.get('connections') || []
connections: persistentStore.get('connections', [])
},
getters: {
getConnections: state => state.connections,

View File

@@ -7,16 +7,18 @@ export default {
namespaced: true,
strict: true,
state: {
locale: persistentStore.get('locale') || 'en-US',
explorebar_size: persistentStore.get('explorebar_size') || null,
notifications_timeout: persistentStore.get('notifications_timeout') || 5,
auto_complete: persistentStore.get('auto_complete') || true,
line_wrap: persistentStore.get('line_wrap') || true,
application_theme: persistentStore.get('application_theme') || 'dark',
editor_theme: persistentStore.get('editor_theme') || 'twilight'
locale: persistentStore.get('locale', 'en-US'),
allow_prerelease: persistentStore.get('allow_prerelease', true),
explorebar_size: persistentStore.get('explorebar_size', null),
notifications_timeout: persistentStore.get('notifications_timeout', 5),
auto_complete: persistentStore.get('auto_complete', true),
line_wrap: persistentStore.get('line_wrap', true),
application_theme: persistentStore.get('application_theme', 'dark'),
editor_theme: persistentStore.get('editor_theme', 'twilight')
},
getters: {
getLocale: state => state.locale,
getAllowPrerelease: state => state.allow_prerelease,
getExplorebarSize: state => state.explorebar_size,
getNotificationsTimeout: state => state.notifications_timeout,
getAutoComplete: state => state.auto_complete,
@@ -30,6 +32,10 @@ export default {
i18n.locale = locale;
persistentStore.set('locale', state.locale);
},
SET_ALLOW_PRERELEASE (state, allow) {
state.allow_prerelease = allow;
persistentStore.set('allow_prerelease', state.allow_prerelease);
},
SET_NOTIFICATIONS_TIMEOUT (state, timeout) {
state.notifications_timeout = timeout;
persistentStore.set('notifications_timeout', state.notifications_timeout);
@@ -54,6 +60,9 @@ export default {
changeLocale ({ commit }, locale) {
commit('SET_LOCALE', locale);
},
changeAllowPrerelease ({ commit }, allow) {
commit('SET_ALLOW_PRERELEASE', allow);
},
updateNotificationsTimeout ({ commit }, timeout) {
commit('SET_NOTIFICATIONS_TIMEOUT', timeout);
},

View File

@@ -1,6 +1,6 @@
'use strict';
import Connection from '@/ipc-api/Connection';
import Database from '@/ipc-api/Database';
import Schema from '@/ipc-api/Schema';
import Users from '@/ipc-api/Users';
import { uidGen } from 'common/libs/uidGen';
const tabIndex = [];
@@ -40,6 +40,12 @@ export default {
.filter(workspace => workspace.connected)
.map(workspace => workspace.uid);
},
getLoadedSchemas: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).loaded_schemas;
},
getSearchTerm: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).search_term;
},
isUnsavedDiscardModal: state => {
return state.is_unsaved_discard_modal;
}
@@ -48,15 +54,19 @@ export default {
SELECT_WORKSPACE (state, uid) {
state.selected_workspace = uid;
},
ADD_CONNECTED (state, { uid, client, dataTypes, indexTypes, structure }) {
ADD_CONNECTED (state, payload) {
const { uid, client, dataTypes, indexTypes, customizations, structure, version } = payload;
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
client,
dataTypes,
indexTypes,
customizations,
structure,
connected: true
connected: true,
version
}
: workspace);
},
@@ -65,6 +75,8 @@ export default {
? {
...workspace,
structure: {},
breadcrumbs: {},
loaded_schemas: new Set(),
connected: false
}
: workspace);
@@ -77,6 +89,19 @@ export default {
}
: workspace);
},
REFRESH_SCHEMA (state, { uid, schema, schemaElements }) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
const schemaIndex = workspace.structure.findIndex(s => s.name === schema);
if (schemaIndex !== -1)
workspace.structure[schemaIndex] = schemaElements;
else
workspace.structure.push(schemaElements);
}
return workspace;
});
},
REFRESH_COLLATIONS (state, { uid, collations }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
@@ -120,7 +145,15 @@ export default {
}
: workspace);
},
NEW_TAB (state, { uid, tab }) {
SET_SEARCH_TERM (state, { uid, term }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
search_term: term
}
: workspace);
},
NEW_TAB (state, { uid, tab, content, autorun }) {
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
const newTab = {
uid: tab,
@@ -128,7 +161,9 @@ export default {
selected: false,
type: 'query',
fields: [],
keyUsage: []
keyUsage: [],
content: content || '',
autorun: !!autorun
};
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
@@ -198,6 +233,13 @@ export default {
},
SET_PENDING_BREADCRUMBS (state, payload) {
state.pending_breadcrumbs = payload;
},
ADD_LOADED_SCHEMA (state, payload) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === payload.uid)
workspace.loaded_schemas.add(payload.schema);
return workspace;
});
}
},
actions: {
@@ -212,20 +254,48 @@ export default {
else {
let dataTypes = [];
let indexTypes = [];
let customizations = {};
switch (connection.client) {
case 'mysql':
case 'maria':
dataTypes = require('common/data-types/mysql');
indexTypes = require('common/index-types/mysql');
customizations = require('common/customizations/mysql');
break;
case 'pg':
dataTypes = require('common/data-types/postgresql');
indexTypes = require('common/index-types/postgresql');
customizations = require('common/customizations/postgresql');
break;
}
const { status, response: version } = await Schema.getVersion(connection.uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: version }, { root: true });
const isMySQL = version.name.includes('MySQL');
if (isMySQL && connection.client !== 'mysql') {
const connProxy = Object.assign({}, connection);
connProxy.client = 'mysql';
dispatch('connections/editConnection', connProxy, { root: true });
}
else if (!isMySQL && connection.client === 'mysql') {
const connProxy = Object.assign({}, connection);
connProxy.client = 'maria';
dispatch('connections/editConnection', connProxy, { root: true });
}
commit('ADD_CONNECTED', {
uid: connection.uid,
client: connection.client,
dataTypes,
indexTypes,
structure: response
customizations,
structure: response,
version
});
dispatch('refreshCollations', connection.uid);
dispatch('refreshVariables', connection.uid);
@@ -237,9 +307,10 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshStructure ({ dispatch, commit }, uid) {
async refreshStructure ({ dispatch, commit, getters }, uid) {
try {
const { status, response } = await Database.getStructure(uid);
const { status, response } = await Schema.getStructure({ uid, schemas: getters.getLoadedSchemas(uid) });
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -249,9 +320,21 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshSchema ({ dispatch, commit }, { uid, schema }) {
try {
const { status, response } = await Schema.getStructure({ uid, schemas: new Set([schema]) });
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('REFRESH_SCHEMA', { uid, schema, schemaElements: response.find(_schema => _schema.name === schema) });
}
catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshCollations ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getCollations(uid);
const { status, response } = await Schema.getCollations(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -263,7 +346,7 @@ export default {
},
async refreshVariables ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getVariables(uid);
const { status, response } = await Schema.getVariables(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -275,7 +358,7 @@ export default {
},
async refreshEngines ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getEngines(uid);
const { status, response } = await Schema.getEngines(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -307,29 +390,20 @@ export default {
uid,
connected: false,
selected_tab: 0,
tabs: [{
uid: 'data',
type: 'table',
fields: [],
keyUsage: []
},
{
uid: 'prop',
type: 'table',
fields: [],
keyUsage: []
}],
search_term: '',
tabs: [],
structure: {},
variables: [],
collations: [],
users: [],
breadcrumbs: {}
breadcrumbs: {},
loaded_schemas: new Set()
};
commit('ADD_WORKSPACE', workspace);
if (getters.getWorkspace(uid).tabs.length < 3)
dispatch('newTab', uid);
dispatch('newTab', { uid });
dispatch('setUnsavedChanges', false);
},
@@ -356,15 +430,21 @@ export default {
if (lastBreadcrumbs.schema === payload.schema && hasLastChildren && !hasChildren) return;
if (lastBreadcrumbs.schema !== payload.schema)
Database.useSchema({ uid: getters.getSelected, schema: payload.schema });
Schema.useSchema({ uid: getters.getSelected, schema: payload.schema });
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...breadcrumbsObj, ...payload } });
lastBreadcrumbs = { ...breadcrumbsObj, ...payload };
if (payload.schema)
commit('ADD_LOADED_SCHEMA', { uid: getters.getSelected, schema: payload.schema });
},
newTab ({ commit }, uid) {
setSearchTerm ({ commit, getters }, term) {
commit('SET_SEARCH_TERM', { uid: getters.getSelected, term });
},
newTab ({ commit }, { uid, content, autorun }) {
const tab = uidGen('T');
commit('NEW_TAB', { uid, tab });
commit('NEW_TAB', { uid, tab, content, autorun });
commit('SELECT_TAB', { uid, tab });
},
removeTab ({ commit }, payload) {