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

Compare commits

...

137 Commits

Author SHA1 Message Date
dea5ec7513 chore(release): 0.1.2 2021-04-11 12:39:56 +02:00
be816e8588 perf(UI): improved setting modal rendering 2021-04-11 12:38:50 +02:00
1e938adc5d feat: in-app last release changelog 2021-04-11 12:35:16 +02:00
8735a0c5f9 feat(PostgreSQL): edit timezone in cell editor 2021-04-11 10:55:22 +02:00
3dde1c109e feat(PostgreSQL): procedures management 2021-04-10 20:38:46 +02:00
d0b3e1b1b8 feat(PostgreSQL): support of arrays in table settings 2021-04-09 19:31:41 +02:00
c20bff7bcb fix: deletion of rows from query results 2021-04-08 21:49:38 +02:00
9f5ec0276c fix: no foreign key select when cell value is NULL, closes #50 2021-04-08 18:02:16 +02:00
55932fe115 fix: cell edit doesn't properly use primary or unique index to update if both present, closes #51 2021-04-08 17:47:10 +02:00
d374372e20 fix: wrong datetime conversion when updating a row without an unique key 2021-04-07 15:05:11 +02:00
bb5f44681f fix(UI): white readonly inputs with dark theme 2021-04-07 09:20:11 +02:00
49a4e1cb7b fix(PostgreSQL): issue with selected schema different than public 2021-04-06 12:48:40 +02:00
c2f76e490a chore(release): 0.1.1 2021-04-03 12:21:34 +02:00
e349dd5eab feat: scratchpad to save persistent notes 2021-04-03 12:17:40 +02:00
280697698e feat(UI): light theme 2021-04-03 11:21:58 +02:00
0783f8b57e chore: update README.md 2021-04-02 15:27:29 +02:00
c981244d7a fix(UI): editor theme preview not properly loaded in some cases 2021-04-01 15:26:49 +02:00
dcb135dd01 fix: hide update tab for Windows Store distributions 2021-04-01 14:12:49 +02:00
99f7511c4d feat(PostgreSQL): views management 2021-03-31 16:54:06 +02:00
fe4c8e12b3 feat(PostgreSQL): foreign keys management 2021-03-31 15:57:23 +02:00
21728a663d chore: update README.md 2021-03-31 10:59:19 +02:00
9ca03f4625 feat(PostgreSQL): indexes management 2021-03-30 19:07:04 +02:00
affb7288b0 chore: appx logos 2021-03-30 19:06:48 +02:00
614e0d3275 feat(PostgreSQL): unique keys management 2021-03-29 20:18:44 +02:00
feef5e30ee feat(PostgreSQL): tables addition 2021-03-28 11:55:15 +02:00
82c25711b6 ci: moving to GitHub actions 2021-03-26 18:03:44 +01:00
2ca2988832 chore: appx build configuration 2021-03-26 16:52:49 +01:00
e3f259c6e8 feat(PostgreSQL): table fields edit 2021-03-25 18:33:29 +01:00
e7401cc96e fix: fields of ref. table not automatically loaded in foreign keys modal 2021-03-22 18:04:19 +01:00
13b9840f3d chore: update README.md 2021-03-21 14:39:11 +01:00
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
125 changed files with 10715 additions and 1419 deletions

View File

@@ -3,7 +3,7 @@ name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: Fabio286
--- ---
@@ -25,13 +25,6 @@ If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. iOS] - 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] - Version [e.g. 22]
**Additional context** **Additional context**

View File

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

26
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Build/release
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v1
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 10
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

View File

@@ -8,7 +8,8 @@
"stylelint-scss" "stylelint-scss"
], ],
"rules": { "rules": {
"at-rule-no-unknown": null "at-rule-no-unknown": null,
"no-descending-specificity": null
}, },
"syntax": "scss" "syntax": "scss"
} }

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,223 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.1.2](https://github.com/Fabio286/antares/compare/v0.1.1...v0.1.2) (2021-04-11)
### Features
* in-app last release changelog ([1e938ad](https://github.com/Fabio286/antares/commit/1e938adc5d8eb5ad16ab16342375eecd88f68d20))
* **PostgreSQL:** edit timezone in cell editor ([8735a0c](https://github.com/Fabio286/antares/commit/8735a0c5f9e5b6b3bcaadf37ce158aa7beae2c48))
* **PostgreSQL:** procedures management ([3dde1c1](https://github.com/Fabio286/antares/commit/3dde1c109e23342d94362626ef7350dc123ea859))
* **PostgreSQL:** support of arrays in table settings ([d0b3e1b](https://github.com/Fabio286/antares/commit/d0b3e1b1b8be9d2c038d70e16d4478671315de8f))
### Bug Fixes
* cell edit doesn't properly use primary or unique index to update if both present, closes [#51](https://github.com/Fabio286/antares/issues/51) ([55932fe](https://github.com/Fabio286/antares/commit/55932fe11583bd5ff48f82b8408965adba4f5071))
* deletion of rows from query results ([c20bff7](https://github.com/Fabio286/antares/commit/c20bff7bcbe340ac99ebcacaba3359edd61c068a))
* no foreign key select when cell value is NULL, closes [#50](https://github.com/Fabio286/antares/issues/50) ([9f5ec02](https://github.com/Fabio286/antares/commit/9f5ec0276c92904975fdaea34b4c845c92bfe8d4))
* wrong datetime conversion when updating a row without an unique key ([d374372](https://github.com/Fabio286/antares/commit/d374372e208318d7e50b258a8041145bdf7992c5))
* **PostgreSQL:** issue with selected schema different than public ([49a4e1c](https://github.com/Fabio286/antares/commit/49a4e1cb7b24642641265d5830d3fee370cceeb4))
* **UI:** white readonly inputs with dark theme ([bb5f446](https://github.com/Fabio286/antares/commit/bb5f44681f87aacf2cd2f60a6d958c5732289790))
### Improvements
* **UI:** improved setting modal rendering ([be816e8](https://github.com/Fabio286/antares/commit/be816e85888b4f3d26cbb9caac0adbc4dde0ea94))
### [0.1.1](https://github.com/Fabio286/antares/compare/v0.1.0...v0.1.1) (2021-04-03)
### Features
* scratchpad to save persistent notes ([e349dd5](https://github.com/Fabio286/antares/commit/e349dd5eaba608591257f2799b830805e4936c27))
* **PostgreSQL:** foreign keys management ([fe4c8e1](https://github.com/Fabio286/antares/commit/fe4c8e12b39dd3cdfc233f07e3fe2ff0676252b0))
* **PostgreSQL:** indexes management ([9ca03f4](https://github.com/Fabio286/antares/commit/9ca03f462560b634970a19d3d97b878d60509acc))
* **PostgreSQL:** table fields edit ([e3f259c](https://github.com/Fabio286/antares/commit/e3f259c6e8327d71bd7dd0a9c33d957dc6ca1fb8))
* **PostgreSQL:** tables addition ([feef5e3](https://github.com/Fabio286/antares/commit/feef5e30eec915cbb219223cc428bd4e98d2e9c5))
* **PostgreSQL:** unique keys management ([614e0d3](https://github.com/Fabio286/antares/commit/614e0d32758c13b59139d349d4682a5bafc3ca88))
* **PostgreSQL:** views management ([99f7511](https://github.com/Fabio286/antares/commit/99f7511c4d5fab4030b30d5134cd03248167faea))
* **UI:** light theme ([2806976](https://github.com/Fabio286/antares/commit/280697698ea5fae6d54326970c823878888c196c))
### Bug Fixes
* **UI:** editor theme preview not properly loaded in some cases ([c981244](https://github.com/Fabio286/antares/commit/c981244d7aa93ca18ca2de44bf8df06f253b9d20))
* fields of ref. table not automatically loaded in foreign keys modal ([e7401cc](https://github.com/Fabio286/antares/commit/e7401cc96e76e00100a88eea9f40541fd8027adb))
* hide update tab for Windows Store distributions ([dcb135d](https://github.com/Fabio286/antares/commit/dcb135dd015b8f8c6cfb44021211bb8cf3089192))
## [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) ### [0.0.13](https://github.com/Fabio286/antares/compare/v0.0.12...v0.0.13) (2021-01-06)

View File

@@ -1,19 +1,19 @@
<p align="center"> <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> </p>
# Antares SQL Client # 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. 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. My 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, it lacks many features** and supports only MySQL. **At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL (partially).
Most of its current features might be enough for basic MySQL use, so give it a chance and send me your feedback, I would really appreciate it. Many of its current features are enough to have a pleasant user experience with MySQL/MariaDB, and basic functionalites with PostgreSQL, so give it a chance and send me your feedback, I would really appreciate it.
I'm actively working on it (yes, i'm a lone dev), hoping to provide cool features and fixes as soon as possible. I'm actively working on it, hoping to provide cool features and fixes as soon as possible.
🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases). 🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest).
👁 To stay tuned for new releases watch this repo on **Release only** channel. 👁 To stay tuned for new releases watch this repo on **Release only** channel.
🌟 Don't forget to **leave a star** if you appreciate this project. 🌟 Don't forget to **leave a star** if you appreciate this project.
@@ -21,7 +21,12 @@ I'm actively working on it (yes, i'm a lone dev), hoping to provide cool feature
Why am I developing an SQL client when there are a lot of them on the market? Why am I developing an SQL client when there are a lot of them on the market?
The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem. The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem.
An application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons or submenu. A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, tabs or submenu.
## Download
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from Microsoft Store](https://raw.githubusercontent.com/Fabio286/antares/gh-pages/src/assets/ms-store.png)](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
🚀 **[Other Downloads](https://github.com/Fabio286/antares/releases/latest)**
## How to contribute ## How to contribute
@@ -32,9 +37,12 @@ An application created with minimalism and semplicity in mind, with features in
- Multiple database connections at same time. - Multiple database connections at same time.
- Database management (add/edit/delete). - Database management (add/edit/delete).
- Full tables management, including indexes and foreign keys. - 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. - Run queries on multiple tabs.
- Query suggestions and auto complete. - Query suggestions and auto complete.
- Native dark theme. - Dark and light theme.
- Scratchpad.
- Multi language. - Multi language.
- Secure password storage. - Secure password storage.
- Auto updates. - Auto updates.
@@ -43,28 +51,36 @@ An application created with minimalism and semplicity in mind, with features in
This is a roadmap with major features will come in near future. 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. - Support for other databases.
- Database tools.
- SSH tunnel support.
- Users management (add/edit/delete).
- UI/UX improvements. - UI/UX improvements.
- Query history. - Query history.
- More context menu shortcuts. - More context menu shortcuts.
- More keyboard shortcuts. - More keyboard shortcuts.
- Query logs console. - Query logs console.
- Fake data filler.
- Import/export and migration. - Import/export and migration.
- Themes.
## 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 ## Currently supported
### Databases ### Databases
- [x] MySQL/MariaDB - [x] MySQL/MariaDB
- [ ] PostgreSQL - [x] PostgreSQL (partially, work in progress)
- [ ] MSSQL
- [ ] SQLite - [ ] SQLite
- [ ] MSSQL
- [ ] OracleDB - [ ] OracleDB
- [ ] More... - [ ] More...
@@ -74,7 +90,7 @@ This is a roadmap with major features will come in near future.
- [x] Windows - [x] Windows
- [x] Linux - [x] Linux
- [x] MacOS (needs tests) - [x] MacOS (not tested due lack of hardware)
#### • ARM #### • ARM
@@ -84,6 +100,11 @@ This is a roadmap with major features will come in near future.
## Translations ## Translations
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/Fabio286/antares/pull/20) **Italian Translation** (46%) / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/Fabio286/antares/pull/29) **Arabic Translation** (45%) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/Fabio286/antares/pull/32) **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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
build/appx/StoreLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.0.13", "version": "0.1.2",
"description": "A cross-platform easy to use SQL client.", "description": "A cross-platform easy to use SQL client.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/Fabio286/antares.git", "repository": "https://github.com/Fabio286/antares.git",
@@ -9,6 +9,7 @@
"dev": "cross-env NODE_ENV=development electron-webpack dev", "dev": "cross-env NODE_ENV=development electron-webpack dev",
"compile": "electron-webpack", "compile": "electron-webpack",
"build": "cross-env NODE_ENV=production npm run compile && electron-builder", "build": "cross-env NODE_ENV=production npm run compile && electron-builder",
"build:appx": "npm run build -- --win appx",
"release": "standard-version", "release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha", "release:pre": "npm run release -- --prerelease alpha",
"test": "npm run lint", "test": "npm run lint",
@@ -19,6 +20,12 @@
"build": { "build": {
"appId": "com.fabio286.antares", "appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"win": {
"target": [
"nsis",
"portable"
]
},
"dmg": { "dmg": {
"contents": [ "contents": [
{ {
@@ -39,6 +46,19 @@
"AppImage" "AppImage"
], ],
"category": "Development" "category": "Development"
},
"appImage": {
"license": "./LICENSE",
"category": "Development"
},
"portable": {
"artifactName": "${productName}-${version}-portable.exe"
},
"appx": {
"displayName": "Antares SQL Client",
"identityName": "62514FabioDiStasio.AntaresSQLClient",
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
"applicationId": "FabioDiStasio.AntaresSQLClient"
} }
}, },
"electronWebpack": { "electronWebpack": {
@@ -47,47 +67,49 @@
} }
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^5.8.55", "@mdi/font": "^5.9.55",
"ace-builds": "^1.4.12", "ace-builds": "^1.4.12",
"electron-log": "^4.3.0", "electron-log": "^4.3.0",
"electron-store": "^6.0.1", "electron-store": "^7.0.0",
"electron-updater": "^4.3.5", "electron-updater": "^4.3.5",
"faker": "^5.3.1",
"keytar": "^7.3.0", "keytar": "^7.3.0",
"lodash": "^4.17.20", "marked": "^2.0.2",
"moment": "^2.29.1", "moment": "^2.29.1",
"mssql": "^6.2.3", "mssql": "^6.2.3",
"mysql": "^2.18.1", "mysql2": "^2.2.5",
"node-sql-parser": "^3.1.0",
"pg": "^8.5.1", "pg": "^8.5.1",
"pgsql-ast-parser": "^7.0.2",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
"spectre.css": "^0.5.9", "spectre.css": "^0.5.9",
"vue-i18n": "^8.22.2", "v-mask": "^2.2.4",
"vue-the-mask": "^0.11.1", "vue-i18n": "^8.22.4",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.6.0", "vuex": "^3.6.0"
"vuex-persist": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"electron": "^11.0.2", "electron": "^11.3.0",
"electron-builder": "^22.9.1", "electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2", "electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0", "electron-webpack-vue": "^2.4.0",
"eslint": "^7.14.0", "eslint": "^7.20.0",
"eslint-config-standard": "^16.0.2", "eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.3.1",
"eslint-plugin-vue": "^7.1.0", "eslint-plugin-vue": "^7.6.0",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"sass-loader": "^10.1.0", "sass-loader": "^10.1.1",
"standard-version": "^9.0.0", "standard-version": "^9.1.0",
"stylelint": "^13.8.0", "stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0", "stylelint-scss": "^3.19.0",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-template-compiler": "^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,56 @@
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,
unsigned: false,
nullable: false,
zerofill: false,
autoIncrement: false,
comment: false,
collation: false,
definer: false,
onUpdate: false,
tableArray: false,
viewAlgorithm: false,
viewSqlSecurity: false,
viewUpdateOption: false,
procedureDeterministic: false,
procedureDataAccess: false,
procedureSql: false,
parametersLength: false
};

View File

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

View File

@@ -0,0 +1,54 @@
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,
unsigned: true,
nullable: true,
zerofill: true,
autoIncrement: true,
comment: true,
collation: true,
definer: true,
onUpdate: true,
viewAlgorithm: true,
viewSqlSecurity: true,
viewUpdateOption: true,
procedureDeterministic: true,
procedureDataAccess: true,
procedureSql: 'BEGIN\r\n\r\nEND',
parametersLength: true
};

View File

@@ -0,0 +1,39 @@
const defaults = require('./defaults');
module.exports = {
...defaults,
// Defaults
defaultPort: 5432,
defaultUser: 'postgres',
defaultDatabase: 'postgres',
// Core
database: true,
// Tools
processesList: true,
// Structure
tables: true,
views: true,
triggers: false,
routines: true,
functions: false,
schedulers: false,
// Settings
tableAdd: true,
viewAdd: true,
triggerAdd: false,
routineAdd: true,
functionAdd: false,
databaseEdit: false,
tableSettings: true,
viewSettings: true,
triggerSettings: false,
routineSettings: true,
functionSettings: false,
schedulerSettings: false,
indexes: true,
foreigns: true,
sortableFields: false,
nullable: true,
tableArray: true,
procedureSql: '$BODY$\r\n\r\n$BODY$'
};

View File

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

View File

@@ -0,0 +1,292 @@
module.exports = [
{
group: 'integer',
types: [
{
name: 'SMALLINT',
length: false,
unsigned: true
},
{
name: 'INTEGER',
length: false,
unsigned: true
},
{
name: 'BIGINT',
length: false,
unsigned: true
},
{
name: 'DECIMAL',
length: false,
unsigned: true
},
{
name: 'NUMERIC',
length: true,
unsigned: true
},
{
name: 'SMALLSERIAL',
length: false,
unsigned: true
},
{
name: 'SERIAL',
length: false,
unsigned: true
},
{
name: 'BIGSERIAL',
length: false,
unsigned: true
}
]
},
{
group: 'float',
types: [
{
name: 'REAL',
length: false,
unsigned: true
},
{
name: 'DOUBLE PRECISION',
length: false,
unsigned: true
}
]
},
{
group: 'monetary',
types: [
{
name: 'money',
length: false,
unsigned: true
}
]
},
{
group: 'string',
types: [
{
name: 'CHARACTER VARYING',
length: true,
unsigned: false
},
{
name: 'CHARACTER',
length: true,
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: false,
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: false,
unsigned: false
},
{
name: 'TIME WITHOUT TIME ZONE',
length: false,
unsigned: false
},
{
name: 'TIME WITH TIME ZONE',
length: false,
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: true,
unsigned: false
},
{
name: 'BIT VARYING',
length: true,
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,82 @@
export const TEXT = ['CHAR', 'VARCHAR']; export const TEXT = [
export const LONG_TEXT = ['TEXT', 'MEDIUMTEXT', 'LONGTEXT']; '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 DATE = ['DATE'];
export const TIME = ['TIME']; export const TIME = [
export const DATETIME = ['DATETIME', 'TIMESTAMP']; 'TIME',
'TIME WITH TIME ZONE'
];
export const BLOB = ['BLOB', 'MEDIUMBLOB', 'LONGBLOB']; export const DATETIME = [
'DATETIME',
'TIMESTAMP',
'TIMESTAMP WITHOUT TIME ZONE',
'TIMESTAMP WITH TIME ZONE'
];
export const BIT = ['BIT']; // Used to check datetime fields only
export const HAS_TIMEZONE = [
'TIMESTAMP WITH TIME ZONE',
'TIME WITH TIME ZONE'
];
export const BLOB = [
'BLOB',
'TINYBLOB',
'MEDIUMBLOB',
'LONGBLOB',
'BYTEA'
];
export const BIT = [
'BIT',
'BIT VARYING'
];

View File

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

View File

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

View File

@@ -7,7 +7,14 @@ export default () => {
}); });
ipcMain.on('get-key', async event => { 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; event.returnValue = key;
}); });
}; };

View File

@@ -1,17 +1,32 @@
import fs from 'fs';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { ClientsFactory } from '../libs/ClientsFactory'; import { ClientsFactory } from '../libs/ClientsFactory';
export default connections => { export default connections => {
ipcMain.handle('test-connection', async (event, conn) => { ipcMain.handle('test-connection', async (event, conn) => {
const params = {
host: conn.host,
port: +conn.port,
user: conn.user,
password: conn.password,
application_name: 'Antares SQL'
};
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({ const connection = ClientsFactory.getConnection({
client: conn.client, client: conn.client,
params: { params
host: conn.host,
port: +conn.port,
user: conn.user,
password: conn.password
}
}); });
await connection.connect(); await connection.connect();
@@ -32,21 +47,36 @@ export default connections => {
}); });
ipcMain.handle('connect', async (event, conn) => { ipcMain.handle('connect', async (event, conn) => {
const connection = ClientsFactory.getConnection({ const params = {
client: conn.client, host: conn.host,
params: { port: +conn.port,
host: conn.host, user: conn.user,
port: +conn.port, password: conn.password,
user: conn.user, application_name: 'Antares SQL'
password: conn.password };
},
poolSize: 1 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 { try {
const connection = ClientsFactory.getConnection({
client: conn.client,
params,
poolSize: 1
});
await connection.connect(); await connection.connect();
const structure = await connection.getStructure(); const structure = await connection.getStructure(new Set());
connections[conn.uid] = connection; 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 views from './views';
import triggers from './triggers'; import triggers from './triggers';
import routines from './routines'; import routines from './routines';
import functions from './functions';
import schedulers from './schedulers';
import updates from './updates'; import updates from './updates';
import application from './application'; import application from './application';
import database from './database'; import schema from './schema';
import users from './users'; import users from './users';
const connections = {}; const connections = {};
@@ -16,7 +18,9 @@ export default () => {
views(connections); views(connections);
triggers(connections); triggers(connections);
routines(connections); routines(connections);
database(connections); functions(connections);
schedulers(connections);
schema(connections);
users(connections); users(connections);
updates(); updates();
application(); 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'; import { ipcMain } from 'electron';
export default connections => { export default connections => {
ipcMain.handle('create-database', async (event, params) => { ipcMain.handle('create-schema', async (event, params) => {
try { try {
const query = `CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`; await connections[params.uid].createSchema(params);
await connections[params.uid].raw(query);
return { status: 'success' }; 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 { try {
const query = `ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`; await connections[params.uid].alterSchema(params);
await connections[params.uid].raw(query);
return { status: 'success' }; 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 { try {
const query = `DROP DATABASE \`${params.database}\``; await connections[params.uid].dropSchema(params);
await connections[params.uid].raw(query);
return { status: 'success' }; 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 { try {
const query = `SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`; const collation = await connections[params.uid].getDatabaseCollation(params);
const collation = await connections[params.uid].raw(query);
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' }; 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 { try {
const structure = await connections[uid].getStructure(); const structure = await connections[params.uid].getStructure(params.schemas);
return { status: 'success', response: structure }; 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 }) => { ipcMain.handle('use-schema', async (event, { uid, schema }) => {
if (!schema) return; if (!schema) return;

View File

@@ -1,6 +1,8 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import faker from 'faker';
import moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper'; 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'; import fs from 'fs';
export default (connections) => { export default (connections) => {
@@ -57,33 +59,94 @@ export default (connections) => {
}); });
ipcMain.handle('update-table-cell', async (event, params) => { ipcMain.handle('update-table-cell', async (event, params) => {
try { try { // TODO: move to client classes
let escapedParam; let escapedParam;
let reload = false; let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`; const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
if (NUMBER.includes(params.type)) if ([...NUMBER, ...FLOAT].includes(params.type))
escapedParam = params.content; escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type)) else if ([...TEXT, ...LONG_TEXT].includes(params.type)) {
escapedParam = `"${sqlEscaper(params.content)}"`; 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)) { else if (BLOB.includes(params.type)) {
if (params.content) { if (params.content) {
const fileBlob = fs.readFileSync(params.content); let fileBlob;
escapedParam = `0x${fileBlob.toString('hex')}`;
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; reload = true;
} }
else else {
escapedParam = '""'; 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 else
escapedParam = `"${sqlEscaper(params.content)}"`; escapedParam = `'${sqlEscaper(params.content)}'`;
await connections[params.uid] if (params.primary) { // TODO: handle multiple primary
.update({ [params.field]: `= ${escapedParam}` }) await connections[params.uid]
.schema(params.schema) .update({ [params.field]: `= ${escapedParam}` })
.from(params.table) .schema(params.schema)
.where({ [params.primary]: `= ${id}` }) .from(params.table)
.run(); .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 } }; return { status: 'success', response: { reload } };
} }
@@ -93,29 +156,56 @@ export default (connections) => {
}); });
ipcMain.handle('delete-table-rows', async (event, params) => { ipcMain.handle('delete-table-rows', async (event, params) => {
let idString; if (params.primary) {
const idString = params.rows.map(row => {
const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary;
if (typeof params.rows[0] === 'string') return typeof row[fieldName] === 'string'
idString = params.rows.map(row => `"${row}"`).join(','); ? `"${row[fieldName]}"`
else : row[fieldName];
idString = params.rows.join(','); }).join(',');
try { try {
const result = await connections[params.uid] const result = await connections[params.uid]
.schema(params.schema) .schema(params.schema)
.delete(params.table) .delete(params.table)
.where({ [params.primary]: `IN (${idString})` }) .where({ [params.primary]: `IN (${idString})` })
.run(); .run();
return { status: 'success', response: result }; return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
} }
catch (err) { else {
return { status: 'error', response: err.toString() }; 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) => { ipcMain.handle('insert-table-rows', async (event, params) => {
try { try { // TODO: move to client classes
const insertObj = {}; const insertObj = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@@ -123,32 +213,166 @@ export default (connections) => {
if (params.row[key] === null) if (params.row[key] === null)
escapedParam = 'NULL'; escapedParam = 'NULL';
else if (NUMBER.includes(type)) else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = params.row[key]; escapedParam = +params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type)) else if ([...TEXT, ...LONG_TEXT].includes(type)) {
escapedParam = `"${sqlEscaper(params.row[key])}"`; switch (connections[params.uid]._client) {
else if (BLOB.includes(type)) { case 'mysql':
if (params.row[key]) { case 'maria':
const fileBlob = fs.readFileSync(params.row[key]); escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
escapedParam = `0x${fileBlob.toString('hex')}`; 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; insertObj[key] = escapedParam;
} }
for (let i = 0; i < params.repeat; i++) { const rows = new Array(+params.repeat).fill(insertObj);
await connections[params.uid]
.schema(params.schema) await connections[params.uid]
.into(params.table) .schema(params.schema)
.insert(insertObj) .into(params.table)
.run(); .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' }; return { status: 'success' };
} }
catch (err) { catch (err) {
@@ -159,13 +383,13 @@ export default (connections) => {
ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => { ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => {
try { try {
const query = connections[uid] const query = connections[uid]
.select(`${column} AS foreignColumn`) .select(`${column} AS foreign_column`)
.schema(schema) .schema(schema)
.from(table) .from(table)
.orderBy('foreignColumn ASC'); .orderBy('foreign_column ASC');
if (description) if (description)
query.select(`LEFT(${description}, 20) AS foreignDescription`); query.select(`LEFT(${description}, 20) AS foreign_description`);
const results = await query.run(); const results = await query.run();

View File

@@ -1,16 +1,21 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
let mainWindow; let mainWindow;
autoUpdater.allowPrerelease = true; autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
export default () => { export default () => {
ipcMain.on('check-for-updates', event => { ipcMain.on('check-for-updates', event => {
mainWindow = event; mainWindow = event;
if (process.windowsStore)
autoUpdater.checkForUpdatesAndNotify().catch(() => { mainWindow.reply('no-auto-update');
mainWindow.reply('check-failed'); else {
}); autoUpdater.checkForUpdatesAndNotify().catch(() => {
mainWindow.reply('check-failed');
});
}
}); });
ipcMain.on('restart-to-update', () => { ipcMain.on('restart-to-update', () => {

View File

@@ -28,7 +28,7 @@ export class AntaresCore {
limit: [], limit: [],
join: [], join: [],
update: [], update: [],
insert: {}, insert: [],
delete: false delete: false
}; };
this._query = Object.assign({}, this._queryDefaults); 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 * @returns
* @memberof AntaresCore * @memberof AntaresCore
*/ */
insert (obj) { insert (arr) {
this._query.insert = { ...this._query.insert, ...obj }; this._query.insert = [...this._query.insert, ...arr];
return this; return this;
} }

View File

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

View File

@@ -1,8 +1,103 @@
'use strict'; 'use strict';
import mysql from 'mysql'; import mysql from 'mysql2';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql';
export class MySQLClient extends AntaresCore { export class MySQLClient extends AntaresCore {
constructor (args) {
super(args);
this._schema = null;
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 * @memberof MySQLClient
*/ */
@@ -27,14 +122,16 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
use (schema) { use (schema) {
this._schema = schema;
return this.raw(`USE \`${schema}\``); return this.raw(`USE \`${schema}\``);
} }
/** /**
* @param {Array} schemas list
* @returns {Array.<Object>} databases scructure * @returns {Array.<Object>} databases scructure
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async getStructure () { async getStructure (schemas) {
const { rows: databases } = await this.raw('SHOW DATABASES'); const { rows: databases } = await this.raw('SHOW DATABASES');
const { rows: functions } = await this.raw('SHOW FUNCTION STATUS'); const { rows: functions } = await this.raw('SHOW FUNCTION STATUS');
const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS'); const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS');
@@ -44,6 +141,8 @@ export class MySQLClient extends AntaresCore {
const triggersArr = []; const triggersArr = [];
for (const db of databases) { for (const db of databases) {
if (!schemas.has(db.Database)) continue;
let { rows: tables } = await this.raw(`SHOW TABLE STATUS FROM \`${db.Database}\``); let { rows: tables } = await this.raw(`SHOW TABLE STATUS FROM \`${db.Database}\``);
if (tables.length) { if (tables.length) {
tables = tables.map(table => { tables = tables.map(table => {
@@ -64,109 +163,121 @@ export class MySQLClient extends AntaresCore {
} }
return databases.map(db => { return databases.map(db => {
// TABLES if (schemas.has(db.Database)) {
const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => { // TABLES
let tableType; const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => {
switch (table.Comment) { let tableType;
case 'VIEW': switch (table.Comment) {
tableType = 'view'; case 'VIEW':
break; tableType = 'view';
default: break;
tableType = 'table'; default:
break; 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 { return {
name: table.Name, name: db.Database,
type: tableType, tables: remappedTables,
rows: table.Rows, functions: remappedFunctions,
created: table.Create_time, procedures: remappedProcedures,
updated: table.Update_time, triggers: remappedTriggers,
engine: table.Engine, schedulers: remappedSchedulers
comment: table.Comment,
size: table.Data_length + table.Index_length,
autoIncrement: table.Auto_increment,
collation: table.Collation
}; };
}); }
else {
// PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
return { return {
name: procedure.Name, name: db.Database,
type: procedure.Type, tables: [],
definer: procedure.Definer, functions: [],
created: procedure.Created, procedures: [],
updated: procedure.Modified, triggers: [],
comment: procedure.Comment, schedulers: []
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: db.Database,
tables: remappedTables,
functions: remappedFunctions,
procedures: remappedProcedures,
triggers: remappedTriggers,
schedulers: remappedSchedulers
};
}); });
} }
@@ -279,13 +390,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 * @returns {Array.<Object>} users list
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async getUsers () { 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 rows.map(row => {
return { return {
@@ -296,6 +407,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 * SHOW CREATE VIEW
* *
@@ -433,19 +582,32 @@ export class MySQLClient extends AntaresCore {
const results = await this.raw(sql); const results = await this.raw(sql);
return results.rows.map(row => { 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'] const parameters = row['Create Procedure']
.match(/(?<=\().*?(?=\))/s)[0] .match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '') .replaceAll('\r', '')
.replaceAll('\t', '') .replaceAll('\t', '')
.slice(1, -1)
.split(',') .split(',')
.map(el => { .map(el => {
const param = el.split(' '); const param = el.split(' ');
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null]; const type = param[2] ? param[2].replace(')', '').split('(') : ['', null];
return { return {
name: param[1] ? param[1].replaceAll('`', '') : '', name: param[1] ? param[1].replaceAll('`', '') : '',
type: type[0], type: type[0].replaceAll('\n', ''),
length: +type[1], length: +type[1] ? +type[1].replace(/\D/g, '') : '',
context: param[0] ? param[0].replace('\n', '') : '' context: param[0] ? param[0].replace('\n', '') : ''
}; };
}).filter(el => el.name); }).filter(el => el.name);
@@ -461,7 +623,7 @@ export class MySQLClient extends AntaresCore {
return { return {
definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0], sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters, parameters: parameters || [],
name: row.Procedure, name: row.Procedure,
comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '', comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER', security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
@@ -511,10 +673,12 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient * @memberof MySQLClient
*/ */
async createRoutine (routine) { async createRoutine (routine) {
const parameters = routine.parameters.reduce((acc, curr) => { const parameters = 'parameters' in routine
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`); ? routine.parameters.reduce((acc, curr) => {
return acc; acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
}, []).join(','); return acc;
}, []).join(',')
: '';
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters}) const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters})
LANGUAGE SQL LANGUAGE SQL
@@ -527,6 +691,221 @@ export class MySQLClient extends AntaresCore {
return await this.raw(sql, { split: false }); 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 * SHOW COLLATION
* *
@@ -589,6 +968,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 * CREATE TABLE
* *
@@ -625,7 +1052,7 @@ export class MySQLClient extends AntaresCore {
options options
} = params; } = params;
let sql = `ALTER TABLE \`${table}\` `; let sql = `ALTER TABLE \`${this._schema}\`.\`${table}\` `;
const alterColumns = []; const alterColumns = [];
// OPTIONS // OPTIONS
@@ -636,7 +1063,8 @@ export class MySQLClient extends AntaresCore {
// ADD FIELDS // ADD FIELDS
additions.forEach(addition => { 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}\` alterColumns.push(`ADD COLUMN \`${addition.name}\`
${addition.type.toUpperCase()}${length ? `(${length})` : ''} ${addition.type.toUpperCase()}${length ? `(${length})` : ''}
@@ -673,7 +1101,8 @@ export class MySQLClient extends AntaresCore {
// CHANGE FIELDS // CHANGE FIELDS
changes.forEach(change => { 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}\` alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\`
${change.type.toUpperCase()}${length ? `(${length})` : ''} ${change.type.toUpperCase()}${length ? `(${length})` : ''}
@@ -795,18 +1224,11 @@ export class MySQLClient extends AntaresCore {
// INSERT // INSERT
let insertRaw = ''; let insertRaw = '';
if (Object.keys(this._query.insert).length) { if (this._query.insert.length) {
const fieldsList = []; const fieldsList = Object.keys(this._query.insert[0]);
const valueList = []; const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`);
const fields = this._query.insert;
for (const key in fields) { insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `;
if (fields[key] === null) continue;
fieldsList.push(key);
valueList.push(fields[key]);
}
insertRaw = `(${fieldsList.join(', ')}) VALUES (${valueList.join(', ')}) `;
} }
// GROUP BY // GROUP BY
@@ -842,49 +1264,51 @@ export class MySQLClient extends AntaresCore {
const nestTables = args.nest ? '.' : false; const nestTables = args.nest ? '.' : false;
const resultsArr = []; const resultsArr = [];
let paramsArr = []; let paramsArr = [];
let selectedFields = [];
const queries = args.split ? sql.split(';') : [sql]; const queries = args.split ? sql.split(';') : [sql];
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
for (const query of queries) { for (const query of queries) {
if (!query) continue; if (!query) continue;
let fieldsArr = []; const timeStart = new Date();
let timeStop;
let keysArr = []; 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) => { this._connection.query({ sql: query, nestTables }, async (err, response, fields) => {
timeStop = new Date();
const queryResult = response; const queryResult = response;
if (err) if (err)
reject(err); reject(err);
else { else {
const remappedFields = fields let remappedFields = fields
? fields.map(field => { ? fields.map(field => {
if (!field || Array.isArray(field))
return false;
const type = this._getType(field);
return { return {
name: field.name, name: field.orgName,
alias: field.name,
orgName: field.orgName, orgName: field.orgName,
schema: field.db, schema: field.schema,
table: field.table, table: field.table,
tableAlias: field.table,
orgTable: field.orgTable, orgTable: field.orgTable,
type: 'VARCHAR' type: type.name,
length: type.length
}; };
}) }).filter(Boolean)
: []; : [];
if (args.details) { if (args.details) {
let cachedTable; let cachedTable;
if (remappedFields.length) { if (remappedFields.length) {
selectedFields = remappedFields.map(field => {
return {
name: field.orgName || field.name,
table: field.orgTable || field.table
};
});
paramsArr = remappedFields.map(field => { 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 { return {
table: field.orgTable || cachedTable, table: field.orgTable || cachedTable,
schema: field.schema || 'INFORMATION_SCHEMA' schema: field.schema || 'INFORMATION_SCHEMA'
@@ -892,43 +1316,16 @@ export class MySQLClient extends AntaresCore {
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i); }).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
for (const paramObj of paramsArr) { for (const paramObj of paramsArr) {
try { // Table data if (!paramObj.table || !paramObj.schema) continue;
try { // Column details
const response = await this.getTableColumns(paramObj); const response = await this.getTableColumns(paramObj);
remappedFields = remappedFields.map(field => {
let detailedFields = response.length const detailedField = response.find(f => f.name === field.name);
? selectedFields.map(selField => { if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema)
return response.find(field => field.name.toLowerCase() === selField.name.toLowerCase() && field.table === selField.table); field = { ...detailedField, ...field };
}).filter(el => !!el) return field;
: []; });
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;
} }
catch (err) { catch (err) {
reject(err); reject(err);
@@ -946,16 +1343,17 @@ export class MySQLClient extends AntaresCore {
} }
resolve({ 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, report: !Array.isArray(queryResult) ? queryResult : false,
fields: fieldsArr.length ? fieldsArr : remappedFields, fields: remappedFields,
keys: keysArr keys: keysArr
}); });
} }
}); });
}); });
resultsArr.push({ rows, report, fields, keys }); resultsArr.push({ rows, report, fields, keys, duration });
} }
return resultsArr.length === 1 ? resultsArr[0] : resultsArr; return resultsArr.length === 1 ? resultsArr[0] : resultsArr;

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<template> <template>
<div id="wrapper"> <div id="wrapper" :class="`theme-${applicationTheme}`">
<TheTitleBar /> <TheTitleBar />
<div id="window-content"> <div id="window-content">
<TheSettingBar /> <TheSettingBar />
@@ -16,6 +16,7 @@
<TheFooter /> <TheFooter />
<TheNotificationsBoard /> <TheNotificationsBoard />
<ModalNewConnection v-if="isNewConnModal" /> <ModalNewConnection v-if="isNewConnModal" />
<TheScratchpad v-if="isScratchpad" />
<ModalSettings v-if="isSettingModal" /> <ModalSettings v-if="isSettingModal" />
<ModalDiscardChanges v-if="isUnsavedDiscardModal" /> <ModalDiscardChanges v-if="isUnsavedDiscardModal" />
</div> </div>
@@ -24,7 +25,7 @@
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import { ipcRenderer } from 'electron'; import { ipcRenderer, remote } from 'electron';
export default { export default {
name: 'App', name: 'App',
@@ -37,6 +38,7 @@ export default {
Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'), Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'),
ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'), ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'),
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'), ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'),
TheScratchpad: () => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad'),
ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges') ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges')
}, },
data () { data () {
@@ -48,16 +50,59 @@ export default {
isNewConnModal: 'application/isNewModal', isNewConnModal: 'application/isNewModal',
isEditModal: 'application/isEditModal', isEditModal: 'application/isEditModal',
isSettingModal: 'application/isSettingModal', isSettingModal: 'application/isSettingModal',
isScratchpad: 'application/isScratchpad',
connections: 'connections/getConnections', connections: 'connections/getConnections',
applicationTheme: 'settings/getApplicationTheme',
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal' isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal'
}) })
}, },
mounted () { mounted () {
ipcRenderer.send('check-for-updates'); ipcRenderer.send('check-for-updates');
this.checkVersionUpdate();
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: { methods: {
...mapActions({ ...mapActions({
showNewConnModal: 'application/showNewConnModal' showNewConnModal: 'application/showNewConnModal',
checkVersionUpdate: 'application/checkVersionUpdate'
}) })
} }
}; };

View File

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

View File

@@ -71,7 +71,6 @@ export default {
<style lang="scss"> <style lang="scss">
.context { .context {
display: flex; display: flex;
color: $body-font-color;
font-size: 16px; font-size: 16px;
z-index: 400; z-index: 400;
justify-content: center; justify-content: center;
@@ -87,7 +86,6 @@ export default {
.context-container { .context-container {
min-width: 100px; min-width: 100px;
z-index: 10; z-index: 10;
box-shadow: 0 0 2px 0 #000;
padding: 0; padding: 0;
background: #1d1d1d; background: #1d1d1d;
border-radius: 0.1rem; border-radius: 0.1rem;
@@ -111,14 +109,10 @@ export default {
position: absolute; position: absolute;
left: 100%; left: 100%;
top: 0; top: 0;
background: #1d1d1d;
box-shadow: 0 0 2px 0 #000;
min-width: 100px; min-width: 100px;
} }
&:hover { &:hover {
background: $primary-color;
.context-submenu { .context-submenu {
display: block; display: block;
visibility: visible; visibility: visible;

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,117 @@
<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 },
showLineNumbers: { type: Boolean, default: true },
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,
showLineNumbers: this.showLineNumbers,
showGutter: this.showLineNumbers
});
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 {
.editor {
width: 100%;
}
}
.ace_.mdi {
display: inline-block;
width: 17px;
}
</style>

View File

@@ -0,0 +1,107 @@
<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-radius: 0.1rem;
height: 1.8rem;
line-height: 1.2rem;
display: flex;
cursor: pointer;
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;
}
.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;
opacity: 0.5;
}
}
</style>

View File

@@ -41,14 +41,16 @@ export default {
localScrollElement: null localScrollElement: null
}; };
}, },
watch: {
scrollElement () {
this.setScrollElement();
}
},
mounted () { mounted () {
this._checkScrollPosition = this.checkScrollPosition.bind(this); this.setScrollElement();
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this._checkScrollPosition);
}, },
beforeDestroy () { beforeDestroy () {
this.localScrollElement.removeEventListener('scroll', this._checkScrollPosition); this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
}, },
methods: { methods: {
checkScrollPosition (e) { checkScrollPosition (e) {
@@ -58,7 +60,7 @@ export default {
this.updateWindow(e); this.updateWindow(e);
}, 200); }, 200);
}, },
updateWindow (e) { updateWindow () {
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight); const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
const totalScrollHeight = this.items.length * this.itemHeight; const totalScrollHeight = this.items.length * this.itemHeight;
const offset = 50; const offset = 50;
@@ -74,6 +76,14 @@ export default {
this.topHeight = firstCutIndex * this.itemHeight; this.topHeight = firstCutIndex * this.itemHeight;
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight; 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>
<option <option
v-for="row in foreignList" v-for="row in foreignList"
:key="row.foreignColumn" :key="row.foreign_column"
:value="row.foreignColumn" :value="row.foreign_column"
:selected="row.foreignColumn === value" :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> </option>
</select> </select>
</template> </template>
@@ -50,11 +50,12 @@ export default {
selectedWorkspace: 'workspaces/getSelected' selectedWorkspace: 'workspaces/getSelected'
}), }),
isValidDefault () { isValidDefault () {
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString()); if (!this.foreignList.length) return true;
return this.value === null || this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString());
} }
}, },
async created () { async created () {
let firstTextField; let foreignDesc;
const params = { const params = {
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
schema: this.keyUsage.refSchema, schema: this.keyUsage.refSchema,
@@ -63,8 +64,10 @@ export default {
try { // Field data try { // Field data
const { status, response } = await Tables.getTableColumns(params); const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') if (status === 'success') {
firstTextField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type)).name || false; const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type));
foreignDesc = textField ? textField.name : false;
}
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
} }
@@ -76,7 +79,7 @@ export default {
const { status, response } = await Tables.getForeignList({ const { status, response } = await Tables.getForeignList({
...params, ...params,
column: this.keyUsage.refField, column: this.keyUsage.refField,
description: firstTextField description: foreignDesc
}); });
if (status === 'success') if (status === 'success')

View File

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

View File

@@ -0,0 +1,124 @@
<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 { NUMBER, FLOAT } from 'common/fieldTypes';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalAskParameters',
components: {
ConfirmModal
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
localRoutine: Object,
client: String
},
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) => {
let qc;
switch (this.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
}
const param = this.localRoutine.parameters.find(param => param.name === curr);
const value = [...NUMBER, ...FLOAT].includes(param.type) ? this.values[curr] : `${qc}${this.values[curr]}${qc}`;
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> </div>
<a class="btn btn-clear c-hand" @click="closeModal" /> <a class="btn btn-clear c-hand" @click="closeModal" />
</div> </div>
<div class="modal-body pb-0"> <div class="modal-body p-0">
<div class="content"> <div class="panel">
<form class="form-horizontal"> <div class="panel-nav">
<fieldset class="m-0" :disabled="isTesting"> <ul class="tab tab-block">
<div class="form-group"> <li
<div class="col-4 col-sm-12"> class="tab-item"
<label class="form-label">{{ $t('word.connectionName') }}:</label> :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>
<div class="col-8 col-sm-12"> <fieldset class="m-0" :disabled="isTesting || !localConnection.ssl">
<input <div class="form-group">
ref="firstInput" <div class="col-4 col-sm-12">
v-model="localConnection.name" <label class="form-label">{{ $t('word.privateKey') }}</label>
class="form-input" </div>
type="text" <div class="col-8 col-sm-12">
> <BaseUploadInput
</div> :value="localConnection.key"
</div> :message="$t('word.browse')"
<div class="form-group"> @clear="pathClear('key')"
<div class="col-4 col-sm-12"> @change="pathSelection($event, 'key')"
<label class="form-label">{{ $t('word.client') }}:</label> />
</div> </div>
<div class="col-8 col-sm-12"> </div>
<select v-model="localConnection.client" class="form-select"> <div class="form-group">
<option value="mysql"> <div class="col-4 col-sm-12">
MySQL <label class="form-label">{{ $t('word.certificate') }}</label>
</option> </div>
<option value="maria"> <div class="col-8 col-sm-12">
MariaDB <BaseUploadInput
</option> :value="localConnection.cert"
<!-- <option value="mssql"> :message="$t('word.browse')"
Microsoft SQL @clear="pathClear('cert')"
</option> @change="pathSelection($event, 'cert')"
<option value="pg"> />
PostgreSQL </div>
</option> </div>
<option value="oracledb"> <div class="form-group">
Oracle DB <div class="col-4 col-sm-12">
</option> --> <label class="form-label">{{ $t('word.caCertificate') }}</label>
</select> </div>
</div> <div class="col-8 col-sm-12">
</div> <BaseUploadInput
<div class="form-group"> :value="localConnection.ca"
<div class="col-4 col-sm-12"> :message="$t('word.browse')"
<label class="form-label">{{ $t('word.hostName') }}/IP:</label> @clear="pathClear('ca')"
</div> @change="pathSelection($event, 'ca')"
<div class="col-8 col-sm-12"> />
<input </div>
v-model="localConnection.host" </div>
class="form-input"
type="text" <div class="form-group">
> <div class="col-4 col-sm-12">
</div> <label class="form-label">{{ $t('word.ciphers') }}</label>
</div> </div>
<div class="form-group"> <div class="col-8 col-sm-12">
<div class="col-4 col-sm-12"> <input
<label class="form-label">{{ $t('word.port') }}:</label> ref="firstInput"
</div> v-model="localConnection.ciphers"
<div class="col-8 col-sm-12"> class="form-input"
<input type="text"
v-model="localConnection.port" >
class="form-input" </div>
type="number" </div>
min="1" </fieldset>
max="65535" </form>
> </div>
</div> <BaseToast
</div> class="mb-2"
<div class="form-group"> :message="toast.message"
<div class="col-4 col-sm-12"> :status="toast.status"
<label class="form-label">{{ $t('word.user') }}:</label> />
</div> </div>
<div class="col-8 col-sm-12"> <div class="modal-footer text-light">
<input <button
v-model="localConnection.user" class="btn btn-gray mr-2"
class="form-input" :class="{'loading': isTesting}"
type="text" @click="startTest"
:disabled="localConnection.ask" >
> {{ $t('message.testConnection') }}
</div> </button>
</div> <button class="btn btn-primary mr-2" @click="saveEditConnection">
<div class="form-group"> {{ $t('word.save') }}
<div class="col-4 col-sm-12"> </button>
<label class="form-label">{{ $t('word.password') }}:</label> <button class="btn btn-link" @click="closeModal">
</div> {{ $t('word.close') }}
<div class="col-8 col-sm-12"> </button>
<input </div>
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> </div>
<BaseToast <ModalAskCredentials
class="mb-2" v-if="isAsking"
:message="toast.message" @close-asking="closeAsking"
:status="toast.status" @credentials="continueTest"
/> />
</div> </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> </div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast'; import BaseToast from '@/components/BaseToast';
import BaseUploadInput from '@/components/BaseUploadInput';
export default { export default {
name: 'ModalEditConnection', name: 'ModalEditConnection',
components: { components: {
ModalAskCredentials, ModalAskCredentials,
BaseToast BaseToast,
BaseUploadInput
}, },
props: { props: {
connection: Object connection: Object
@@ -167,9 +283,15 @@ export default {
}, },
isTesting: false, isTesting: false,
isAsking: false, isAsking: false,
localConnection: null localConnection: null,
selectedTab: 'general'
}; };
}, },
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () { created () {
this.localConnection = Object.assign({}, this.connection); this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
@@ -240,6 +362,21 @@ export default {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') if (e.key === 'Escape')
this.closeModal(); 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> <style scoped>
.modal-container { .modal-container {
position: absolute;
max-width: 450px; max-width: 450px;
top: 17.5vh;
} }
</style> </style>

View File

@@ -5,7 +5,7 @@
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editDatabase') }} <i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editSchema') }}
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -15,7 +15,7 @@
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.name') }}:</label> <label class="form-label">{{ $t('word.name') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
@@ -23,14 +23,14 @@
class="form-input" class="form-input"
type="text" type="text"
required required
:placeholder="$t('message.databaseName')" :placeholder="$t('message.schemaName')"
readonly readonly
> >
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.collation') }}:</label> <label class="form-label">{{ $t('word.collation') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<select <select
@@ -53,7 +53,7 @@
</div> </div>
</div> </div>
<div class="modal-footer text-light"> <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') }} {{ $t('word.update') }}
</button> </button>
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
@@ -66,10 +66,10 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
export default { export default {
name: 'ModalEditDatabase', name: 'ModalEditSchema',
props: { props: {
selectedDatabase: String selectedDatabase: String
}, },
@@ -98,7 +98,7 @@ export default {
async created () { async created () {
let actualCollation; let actualCollation;
try { 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') if (status === 'success')
actualCollation = response; actualCollation = response;
@@ -130,10 +130,10 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
async updateDatabase () { async updateSchema () {
if (this.database.collation !== this.database.prevCollation) { if (this.database.collation !== this.database.prevCollation) {
try { try {
const { status, response } = await Database.updateDatabase({ const { status, response } = await Schema.updateSchema({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
...this.database ...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> </div>
<a class="btn btn-clear c-hand" @click="closeModal" /> <a class="btn btn-clear c-hand" @click="closeModal" />
</div> </div>
<div class="modal-body pb-0"> <div class="modal-body p-0">
<div class="content"> <div class="panel">
<form class="form-horizontal"> <div class="panel-nav">
<fieldset class="m-0" :disabled="isTesting"> <ul class="tab tab-block">
<div class="form-group"> <li
<div class="col-4 col-sm-12"> class="tab-item"
<label class="form-label">{{ $t('word.connectionName') }}:</label> :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>
<div class="col-8 col-sm-12"> <fieldset class="m-0" :disabled="isTesting || !connection.ssl">
<input <div class="form-group">
ref="firstInput" <div class="col-4 col-sm-12">
v-model="connection.name" <label class="form-label">{{ $t('word.privateKey') }}</label>
class="form-input" </div>
type="text" <div class="col-8 col-sm-12">
> <BaseUploadInput
</div> :value="connection.key"
</div> :message="$t('word.browse')"
<div class="form-group"> @clear="pathClear('key')"
<div class="col-4 col-sm-12"> @change="pathSelection($event, 'key')"
<label class="form-label">{{ $t('word.client') }}:</label> />
</div> </div>
<div class="col-8 col-sm-12"> </div>
<select <div class="form-group">
v-model="connection.client" <div class="col-4 col-sm-12">
class="form-select" <label class="form-label">{{ $t('word.certificate') }}</label>
@change="setDefaults" </div>
> <div class="col-8 col-sm-12">
<option value="mysql"> <BaseUploadInput
MySQL :value="connection.cert"
</option> :message="$t('word.browse')"
<option value="maria"> @clear="pathClear('cert')"
MariaDB @change="pathSelection($event, 'cert')"
</option> />
<!-- <option value="mssql"> </div>
Microsoft SQL </div>
</option> <div class="form-group">
<option value="pg"> <div class="col-4 col-sm-12">
PostgreSQL <label class="form-label">{{ $t('word.caCertificate') }}</label>
</option> </div>
<option value="oracledb"> <div class="col-8 col-sm-12">
Oracle DB <BaseUploadInput
</option> --> :value="connection.ca"
</select> :message="$t('word.browse')"
</div> @clear="pathClear('ca')"
</div> @change="pathSelection($event, 'ca')"
<div class="form-group"> />
<div class="col-4 col-sm-12"> </div>
<label class="form-label">{{ $t('word.hostName') }}/IP:</label> </div>
</div>
<div class="col-8 col-sm-12"> <div class="form-group">
<input <div class="col-4 col-sm-12">
v-model="connection.host" <label class="form-label">{{ $t('word.ciphers') }}</label>
class="form-input" </div>
type="text" <div class="col-8 col-sm-12">
> <input
</div> ref="firstInput"
</div> v-model="connection.ciphers"
<div class="form-group"> class="form-input"
<div class="col-4 col-sm-12"> type="text"
<label class="form-label">{{ $t('word.port') }}:</label> >
</div> </div>
<div class="col-8 col-sm-12"> </div>
<input </fieldset>
v-model="connection.port" </form>
class="form-input" </div>
type="number" <BaseToast
min="1" class="mb-2"
max="65535" :message="toast.message"
> :status="toast.status"
</div> />
</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> </div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div> </div>
<div class="modal-footer text-light"> <div class="modal-footer text-light">
<button <button
@@ -150,16 +263,19 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast'; import BaseToast from '@/components/BaseToast';
import BaseUploadInput from '@/components/BaseUploadInput';
export default { export default {
name: 'ModalNewConnection', name: 'ModalNewConnection',
components: { components: {
ModalAskCredentials, ModalAskCredentials,
BaseToast BaseToast,
BaseUploadInput
}, },
data () { data () {
return { return {
@@ -167,21 +283,35 @@ export default {
name: '', name: '',
client: 'mysql', client: 'mysql',
host: '127.0.0.1', host: '127.0.0.1',
port: '3306', database: null,
user: 'root', port: null,
user: null,
password: '', password: '',
ask: false, ask: false,
uid: uidGen('C') uid: uidGen('C'),
ssl: false,
cert: '',
key: '',
ca: '',
ciphers: ''
}, },
toast: { toast: {
status: '', status: '',
message: '' message: ''
}, },
isTesting: false, isTesting: false,
isAsking: false isAsking: false,
selectedTab: 'general'
}; };
}, },
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () { created () {
this.setDefaults();
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => { setTimeout(() => {
@@ -197,20 +327,9 @@ export default {
addConnection: 'connections/addConnection' addConnection: 'connections/addConnection'
}), }),
setDefaults () { setDefaults () {
switch (this.connection.client) { this.connection.user = this.customizations.defaultUser;
case 'mysql': this.connection.port = this.customizations.defaultPort;
this.connection.port = '3306'; this.connection.database = this.customizations.defaultDatabase;
break;
case 'mssql':
this.connection.port = '1433';
break;
case 'pg':
this.connection.port = '5432';
break;
case 'oracledb':
this.connection.port = '1521';
break;
}
}, },
async startTest () { async startTest () {
this.isTesting = true; this.isTesting = true;
@@ -265,6 +384,21 @@ export default {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') if (e.key === 'Escape')
this.closeModal(); 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> <style scoped>
.modal-container { .modal-container {
position: absolute;
max-width: 450px; max-width: 450px;
top: 17.5vh;
} }
</style> </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

@@ -25,7 +25,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.definer" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.definer') }} {{ $t('word.definer') }}
</label> </label>
@@ -53,7 +53,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -76,7 +76,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.dataAccess') }} {{ $t('message.dataAccess') }}
</label> </label>
@@ -89,7 +89,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -117,7 +117,7 @@ export default {
return { return {
localRoutine: { localRoutine: {
definer: '', definer: '',
sql: 'BEGIN\r\n\r\nEND', sql: '',
parameters: [], parameters: [],
name: '', name: '',
comment: '', comment: '',
@@ -131,9 +131,14 @@ export default {
computed: { computed: {
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {
if (this.customizations.procedureSql)
this.localRoutine.sql = this.customizations.procedureSql;
setTimeout(() => { setTimeout(() => {
this.$refs.firstInput.focus(); this.$refs.firstInput.focus();
}, 20); }, 20);

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

View File

@@ -25,7 +25,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -37,7 +37,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.collation') }} {{ $t('word.collation') }}
</label> </label>
@@ -53,7 +53,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.engine') }} {{ $t('word.engine') }}
</label> </label>
@@ -103,10 +103,14 @@ export default {
getDatabaseVariable: 'workspaces/getDatabaseVariable' getDatabaseVariable: 'workspaces/getDatabaseVariable'
}), }),
defaultCollation () { defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || ''; if (this.workspace.customizations.collations)
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
return '';
}, },
defaultEngine () { defaultEngine () {
return this.workspace.engines.find(engine => engine.isDefault).name; if (this.workspace.customizations.engines)
return this.workspace.engines.find(engine => engine.isDefault).name;
return '';
} }
}, },
mounted () { mounted () {

View File

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

View File

@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="column col-6"> <div class="column col-6">
<div class="form-group"> <div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ $t('word.definer') }}</label>
<select v-model="localView.definer" class="form-select"> <select v-model="localView.definer" class="form-select">
<option value=""> <option value="">
@@ -44,7 +44,7 @@
</div> </div>
<div class="columns"> <div class="columns">
<div class="column col-4"> <div class="column col-4">
<div class="form-group"> <div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label> <label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio"> <label class="form-radio">
<input <input
@@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="column col-4"> <div class="column col-4">
<div class="form-group"> <div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label> <label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio"> <label class="form-radio">
<input <input
@@ -99,7 +99,7 @@
</div> </div>
</div> </div>
<div class="column col-4"> <div class="column col-4">
<div class="form-group"> <div v-if="workspace.customizations.viewUpdateOption" class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label> <label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio"> <label class="form-radio">
<input <input

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

@@ -30,12 +30,21 @@
<a class="c-hand">{{ $t('word.themes') }}</a> <a class="c-hand">{{ $t('word.themes') }}</a>
</li> </li>
<li <li
v-if="updateStatus !== 'disabled'"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'update'}" :class="{'active': selectedTab === 'update'}"
@click="selectTab('update')" @click="selectTab('update')"
> >
<a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a> <a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
</li> </li>
<li
v-if="updateStatus !== 'disabled'"
class="tab-item"
:class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')"
>
<a class="c-hand">{{ $t('word.changelog') }}</a>
</li>
<li <li
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'about'}" :class="{'active': selectedTab === 'about'}"
@@ -45,7 +54,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="selectedTab === 'general'" class="panel-body py-4"> <div v-show="selectedTab === 'general'" class="panel-body py-4">
<div class="container"> <div class="container">
<form class="form-horizontal columns"> <form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1"> <div class="column col-12 h6 text-uppercase mb-1">
@@ -56,7 +65,7 @@
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
<label class="form-label"> <label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" /> <i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }}: {{ $t('word.language') }}
</label> </label>
</div> </div>
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
@@ -78,7 +87,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.notificationsTimeout') }}: {{ $t('message.notificationsTimeout') }}
</label> </label>
</div> </div>
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
@@ -103,7 +112,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('word.autoCompletion') }}: {{ $t('word.autoCompletion') }}
</label> </label>
</div> </div>
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
@@ -118,7 +127,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.wrapLongLines') }}: {{ $t('message.wrapLongLines') }}
</label> </label>
</div> </div>
<div class="col-6 col-sm-12"> <div class="col-6 col-sm-12">
@@ -133,26 +142,35 @@
</div> </div>
</div> </div>
<div v-if="selectedTab === 'themes'" class="panel-body py-4"> <div v-show="selectedTab === 'themes'" class="panel-body py-4">
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
<div class="column col-12 h6 text-uppercase mb-2"> <div class="column col-12 h6 text-uppercase mb-2">
{{ $t('message.applicationTheme') }} {{ $t('message.applicationTheme') }}
</div> </div>
<div class="column col-6 c-hand theme-block" :class="{'selected': applicationTheme === 'dark'}"> <div
class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'dark'}"
@click="changeApplicationTheme('dark')"
>
<img :src="require('@/images/dark.png').default" class="img-responsive img-fit-cover s-rounded"> <img :src="require('@/images/dark.png').default" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name"> <div class="theme-name text-light">
<i class="mdi mdi-moon-waning-crescent mdi-48px" /> <i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
{{ $t('word.dark') }} {{ $t('word.dark') }}
</div> </div>
</div> </div>
</div> </div>
<div class="column col-6 theme-block disabled" :class="{'selected': applicationTheme === 'light'}"> <div
<div class="theme-name"> class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'light'}"
@click="changeApplicationTheme('light')"
>
<img :src="require('@/images/light.png').default" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-dark">
<i class="mdi mdi-white-balance-sunny mdi-48px" /> <i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
{{ $t('word.light') }} (Coming) {{ $t('word.light') }}
</div> </div>
</div> </div>
</div> </div>
@@ -185,8 +203,9 @@
</select> </select>
</div> </div>
<div class="column col-12"> <div class="column col-12">
<QueryEditor <BaseTextEditor
:value="exampleQuery" :value="exampleQuery"
mode="sql"
:workspace="workspace" :workspace="workspace"
:read-only="true" :read-only="true"
:height="270" :height="270"
@@ -196,18 +215,21 @@
</div> </div>
</div> </div>
<div v-if="selectedTab === 'update'" class="panel-body py-4"> <div v-show="selectedTab === 'update'" class="panel-body py-4">
<ModalSettingsUpdate /> <ModalSettingsUpdate />
</div> </div>
<div v-show="selectedTab === 'changelog'" class="panel-body py-4">
<ModalSettingsChangelog />
</div>
<div v-if="selectedTab === 'about'" class="panel-body py-4"> <div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center"> <div class="text-center">
<img :src="require('@/images/logo.svg').default" width="128"> <img :src="require('@/images/logo.svg').default" width="128">
<h4>{{ appName }}</h4> <h4>{{ appName }}</h4>
<p> <p>
{{ $t('word.version') }}: {{ appVersion }}<br> {{ $t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a><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('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
<small>{{ $t('message.madeWithJS') }}</small> <small>{{ $t('message.madeWithJS') }}</small>
</p> </p>
</div> </div>
@@ -222,14 +244,16 @@
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import localesNames from '@/i18n/supported-locales'; import localesNames from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate'; import ModalSettingsUpdate from '@/components/ModalSettingsUpdate';
import QueryEditor from '@/components/QueryEditor'; import ModalSettingsChangelog from '@/components/ModalSettingsChangelog';
import BaseTextEditor from '@/components/BaseTextEditor';
const { shell } = require('electron'); const { shell } = require('electron');
export default { export default {
name: 'ModalSettings', name: 'ModalSettings',
components: { components: {
ModalSettingsUpdate, ModalSettingsUpdate,
QueryEditor ModalSettingsChangelog,
BaseTextEditor
}, },
data () { data () {
return { return {
@@ -350,6 +374,7 @@ ORDER BY
changeLocale: 'settings/changeLocale', changeLocale: 'settings/changeLocale',
changeAutoComplete: 'settings/changeAutoComplete', changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap', changeLineWrap: 'settings/changeLineWrap',
changeApplicationTheme: 'settings/changeApplicationTheme',
changeEditorTheme: 'settings/changeEditorTheme', changeEditorTheme: 'settings/changeEditorTheme',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout' updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}), }),
@@ -382,54 +407,58 @@ ORDER BY
<style lang="scss"> <style lang="scss">
#settings { #settings {
.modal-body { .modal-container {
overflow: hidden; position: absolute;
top: 17.5vh;
.panel-body { .modal-body {
height: calc(70vh - 70px); overflow: hidden;
overflow: auto;
.theme-block { .panel-body {
position: relative; min-height: calc(25vh - 70px);
text-align: center; overflow: auto;
&.selected { .theme-block {
img { position: relative;
box-shadow: 0 0 0 3px $primary-color; text-align: center;
&.selected {
img {
box-shadow: 0 0 0 3px $primary-color;
}
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.theme-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
top: 0;
height: 100%;
width: 100%;
} }
} }
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.theme-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
top: 0;
height: 100%;
width: 100%;
text-shadow: 0 0 8px #000;
}
} }
}
.badge::after { .badge::after {
background: #32b643; background: #32b643;
} }
.badge-update::after { .badge-update::after {
bottom: initial; bottom: initial;
background: $primary-color; background: $primary-color;
} }
.form-label { .form-label {
display: flex; display: flex;
align-items: center; align-items: center;
}
} }
} }
} }

View File

@@ -0,0 +1,78 @@
<template>
<div class="p-relative">
<BaseLoader v-if="isLoading" />
<div
id="changelog"
class="container"
v-html="changelog"
/>
<div v-if="isError" class="empty">
<div class="empty-icon">
<i class="mdi mdi-48px mdi-alert-outline" />
</div>
</div>
</div>
</template>
<script>
import marked from 'marked';
import BaseLoader from '@/components/BaseLoader';
export default {
name: 'ModalSettingsChangelog',
components: {
BaseLoader
},
data () {
return {
changelog: '',
isLoading: true,
error: '',
isError: false
};
},
created () {
this.getChangelog();
},
methods: {
async getChangelog () {
try {
const apiRes = await fetch('https://api.github.com/repos/Fabio286/antares/releases/latest', {
method: 'GET'
});
const { body } = await apiRes.json();
const markdown = body.substr(0, body.indexOf('### Download'));
const renderer = {
link (href, title, text) {
return text;
},
listitem (text) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
}
};
marked.use({ renderer });
this.changelog = marked(markdown);
}
catch (err) {
this.error = err.message;
this.isError = true;
}
this.isLoading = false;
}
}
};
</script>
<style lang="scss">
#changelog {
h3 {
font-size: 1rem;
}
li {
margin-top: 0;
}
}
</style>

View File

@@ -36,11 +36,17 @@
{{ $t('message.restartToInstall') }} {{ $t('message.restartToInstall') }}
</button> </button>
</div> </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> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
export default { export default {
@@ -48,7 +54,8 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
updateStatus: 'application/getUpdateStatus', updateStatus: 'application/getUpdateStatus',
downloadPercentage: 'application/getDownloadProgress' downloadPercentage: 'application/getDownloadProgress',
allowPrerelease: 'settings/getAllowPrerelease'
}), }),
updateMessage () { updateMessage () {
switch (this.updateStatus) { switch (this.updateStatus) {
@@ -70,18 +77,18 @@ export default {
} }
}, },
methods: { methods: {
...mapActions({
changeAllowPrerelease: 'settings/changeAllowPrerelease'
}),
checkForUpdates () { checkForUpdates () {
ipcRenderer.send('check-for-updates'); ipcRenderer.send('check-for-updates');
}, },
restartToUpdate () { restartToUpdate () {
ipcRenderer.send('restart-to-update'); ipcRenderer.send('restart-to-update');
},
toggleAllowPrerelease () {
this.changeAllowPrerelease(!this.allowPrerelease);
} }
} }
}; };
</script> </script>
<style lang="scss">
.empty {
color: $body-font-color;
}
</style>

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> <template>
<div class="editor-wrapper"> <div class="editor-wrapper">
<div <div
ref="editor" :id="`editor-${id}`"
class="editor" class="editor"
:style="{height: `${height}px`}" :style="{height: `${height}px`}"
/> />
@@ -12,7 +12,7 @@
import * as ace from 'ace-builds'; import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver'; import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools'; import '../libs/ext-language_tools';
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
export default { export default {
@@ -20,6 +20,7 @@ export default {
props: { props: {
value: String, value: String,
workspace: Object, workspace: Object,
isSelected: Boolean,
schema: { type: String, default: '' }, schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false }, autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false }, readOnly: { type: Boolean, default: false },
@@ -29,14 +30,17 @@ export default {
return { return {
editor: null, editor: null,
fields: [], fields: [],
baseCompleter: [] customCompleter: [],
id: null,
lastSchema: null
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
editorTheme: 'settings/getEditorTheme', editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete', autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap' lineWrap: 'settings/getLineWrap',
baseCompleter: 'application/getBaseCompleter'
}), }),
tables () { tables () {
return this.workspace return this.workspace
@@ -47,13 +51,68 @@ export default {
}, []).map(table => { }, []).map(table => {
return { return {
name: table.name, name: table.name,
comment: table.comment,
type: table.type, type: table.type,
fields: [] 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 () { mode () {
switch (this.workspace.client) { switch (this.workspace.client) {
case 'mysql': case 'mysql':
@@ -108,10 +167,24 @@ export default {
wrap: this.lineWrap 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 () { mounted () {
this.editor = ace.edit(this.$refs.editor, { this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`, mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`, theme: `ace/theme/${this.editorTheme}`,
value: this.value, value: this.value,
@@ -127,21 +200,10 @@ export default {
enableLiveAutocompletion: this.autoComplete enableLiveAutocompletion: this.autoComplete
}); });
this.editor.completers.push({ if (!this.baseCompleter.length)
getCompletions: (editor, session, pos, prefix, callback) => { this.setBaseCompleters(this.editor.completers.map(el => Object.assign({}, el)));
const completions = [];
this.tables.forEach(table => {
completions.push({
value: table.name,
meta: table.type,
caption: table.comment
});
});
callback(null, completions);
}
});
this.baseCompleter = this.editor.completers; this.setCustomCompleter();
this.editor.commands.on('afterExec', e => { this.editor.commands.on('afterExec', e => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) { if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
@@ -164,13 +226,13 @@ export default {
}).catch(console.log); }).catch(console.log);
} }
else else
this.editor.completers = this.baseCompleter; this.editor.completers = this.customCompleter;
} }
else else
this.editor.completers = this.baseCompleter; this.editor.completers = this.customCompleter;
} }
else else
this.editor.completers = this.baseCompleter; this.editor.completers = this.customCompleter;
} }
}); });
@@ -179,6 +241,22 @@ export default {
this.$emit('update:value', content); 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) { if (this.autoFocus) {
setTimeout(() => { setTimeout(() => {
this.editor.focus(); this.editor.focus();
@@ -189,6 +267,33 @@ export default {
setTimeout(() => { setTimeout(() => {
this.editor.resize(); this.editor.resize();
}, 20); }, 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> </script>
@@ -207,16 +312,20 @@ export default {
width: 17px; width: 17px;
} }
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line { .ace_gutter-cell.ace_breakpoint {
background-color: #c9561a99; &::before {
} content: '\F0403';
position: absolute;
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover { left: 3px;
background-color: #c9571a33; top: 2px;
border: none; color: $primary-color;
} display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons", sans-serif;
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight { font-size: inherit;
color: #e0d00c; text-rendering: auto;
line-height: inherit;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
} }
</style> </style>

View File

@@ -2,9 +2,9 @@
<div id="footer" class="text-light"> <div id="footer" class="text-light">
<div class="footer-left-elements"> <div class="footer-left-elements">
<ul class="footer-elements"> <ul class="footer-elements">
<li class="footer-element" :title="$t('word.version')"> <li class="footer-element">
<i class="mdi mdi-18px mdi-memory mr-1" /> <i class="mdi mdi-18px mdi-database mr-1" />
<small>{{ appVersion }}</small> <small>{{ versionString }}</small>
</li> </li>
</ul> </ul>
</div> </div>
@@ -34,9 +34,18 @@ export default {
name: 'TheFooter', name: 'TheFooter',
computed: { computed: {
...mapGetters({ ...mapGetters({
appName: 'application/appName', workspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
appVersion: 'application/appVersion' 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: { methods: {
...mapActions({ ...mapActions({
@@ -55,13 +64,11 @@ export default {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: $primary-color;
padding: 0 0.2rem; padding: 0 0.2rem;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
box-shadow: 0 0 1px 0 #000;
.footer-elements { .footer-elements {
list-style: none; list-style: none;
@@ -79,10 +86,6 @@ export default {
&.footer-link { &.footer-link {
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
&:hover {
background: rgba($color: #fff, $alpha: 0.1);
}
} }
} }
} }

View File

@@ -0,0 +1,75 @@
<template>
<ConfirmModal
:confirm-text="$t('word.update')"
:cancel-text="$t('word.close')"
size="large"
:hide-footer="true"
@hide="hideScratchpad"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-notebook-edit-outline mr-1" /> {{ $t('word.scratchpad') }}
</div>
</template>
<div :slot="'body'">
<div>
<div>
<TextEditor
:value.sync="localNotes"
editor-class="textarea-editor"
mode="markdown"
:auto-focus="true"
:show-line-numbers="false"
/>
</div>
<small class="text-gray">{{ $t('message.markdownSupported') }}</small>
</div>
</div>
</ConfirmModal>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
export default {
name: 'TheScratchpad',
components: {
ConfirmModal,
TextEditor
},
data () {
return {
localNotes: '',
debounceTimeout: null
};
},
computed: {
...mapGetters({
notes: 'scratchpad/getNotes'
})
},
watch: {
localNotes () {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => {
this.changeNotes(this.localNotes);
}, 200);
}
},
created () {
this.localNotes = this.notes;
},
methods: {
...mapActions({
hideScratchpad: 'application/hideScratchpad',
changeNotes: 'scratchpad/changeNotes'
}),
hideModal () {
this.$emit('hide');
}
}
};
</script>

View File

@@ -36,6 +36,10 @@
<div class="settingbar-bottom-elements"> <div class="settingbar-bottom-elements">
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<li class="settingbar-element btn btn-link ex-tooltip" @click="showScratchpad">
<i class="settingbar-element-icon mdi mdi-24px mdi-notebook-edit-outline text-light" />
<span class="ex-tooltip-content">{{ $t('word.scratchpad') }}</span>
</li>
<li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')"> <li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')">
<i class="settingbar-element-icon mdi mdi-24px mdi-cog text-light" :class="{' badge badge-update': hasUpdates}" /> <i class="settingbar-element-icon mdi mdi-24px mdi-cog text-light" :class="{' badge badge-update': hasUpdates}" />
<span class="ex-tooltip-content">{{ $t('word.settings') }}</span> <span class="ex-tooltip-content">{{ $t('word.settings') }}</span>
@@ -90,6 +94,7 @@ export default {
updateConnections: 'connections/updateConnections', updateConnections: 'connections/updateConnections',
showNewConnModal: 'application/showNewConnModal', showNewConnModal: 'application/showNewConnModal',
showSettingModal: 'application/showSettingModal', showSettingModal: 'application/showSettingModal',
showScratchpad: 'application/showScratchpad',
selectWorkspace: 'workspaces/selectWorkspace' selectWorkspace: 'workspaces/selectWorkspace'
}), }),
contextMenu (event, connection) { contextMenu (event, connection) {
@@ -117,9 +122,7 @@ export default {
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: $bg-color-light;
padding: 0; padding: 0;
box-shadow: 0 0 1px 0 #000;
z-index: 9; z-index: 9;
.settingbar-top-elements { .settingbar-top-elements {
@@ -134,7 +137,6 @@ export default {
.settingbar-bottom-elements { .settingbar-bottom-elements {
padding-top: 0.5rem; padding-top: 0.5rem;
background: $bg-color-light;
z-index: 1; z-index: 1;
} }
@@ -162,7 +164,6 @@ export default {
} }
&.selected { &.selected {
border-left-color: $body-font-color;
opacity: 1; opacity: 1;
} }
@@ -171,12 +172,10 @@ export default {
bottom: -10px; bottom: -10px;
right: 0; right: 0;
position: absolute; position: absolute;
background: $success-color;
} }
&.badge-update::after { &.badge-update::after {
bottom: initial; bottom: initial;
background: $primary-color;
} }
} }
} }

View File

@@ -102,12 +102,10 @@ export default {
display: flex; display: flex;
position: relative; position: relative;
justify-content: space-between; justify-content: space-between;
background: $bg-color-light;
align-items: center; align-items: center;
height: $titlebar-height; height: $titlebar-height;
-webkit-app-region: drag; -webkit-app-region: drag;
user-select: none; user-select: none;
box-shadow: 0 0 1px 0 #000;
z-index: 9999; z-index: 9999;
.titlebar-resizer { .titlebar-resizer {
@@ -149,11 +147,6 @@ export default {
&:hover { &:hover {
opacity: 1; opacity: 1;
background: rgba($color: #fff, $alpha: 0.2);
}
&.close-button:hover {
background: red;
} }
} }
} }

View File

@@ -7,20 +7,52 @@
ref="tabWrap" ref="tabWrap"
class="tab tab-block column col-12" class="tab tab-block column col-12"
> >
<li class="tab-item d-none"> <li class="tab-item dropdown tools-dropdown">
<a class="tab-link workspace-tools-link"> <a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
>
<i class="mdi mdi-24px mdi-tools" /> <i class="mdi mdi-24px mdi-tools" />
</a> </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>
<li <li
v-if="schemaChild" v-if="schemaChild && isSettingSupported"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'prop'}" :class="{'active': selectedTab === 'prop'}"
@click="selectTab({uid: workspace.uid, tab: 'prop'})" @click="selectTab({uid: workspace.uid, tab: 'prop'})"
> >
<a class="tab-link"> <a class="tab-link">
<i class="mdi mdi-18px mdi-tune mr-1" /> <i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="schemaChild">{{ $t('word.properties').toUpperCase() }}: {{ schemaChild }}</span> <span :title="schemaChild">{{ $t('word.settings').toUpperCase() }}: {{ schemaChild }}</span>
</a> </a>
</li> </li>
<li <li
@@ -89,6 +121,18 @@
:connection="connection" :connection="connection"
:routine="workspace.breadcrumbs.procedure" :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 <WorkspaceTableTab
v-show="selectedTab === 'data'" v-show="selectedTab === 'data'"
:connection="connection" :connection="connection"
@@ -97,11 +141,16 @@
<WorkspaceQueryTab <WorkspaceQueryTab
v-for="tab of queryTabs" v-for="tab of queryTabs"
:key="tab.uid" :key="tab.uid"
:tab-uid="tab.uid" :tab="tab"
:is-selected="selectedTab === tab.uid" :is-selected="selectedTab === tab.uid"
:connection="connection" :connection="connection"
/> />
</div> </div>
<ModalProcessesList
v-if="isProcessesModal"
:connection="connection"
@close="hideProcessesModal"
/>
</div> </div>
</template> </template>
@@ -115,6 +164,9 @@ import WorkspacePropsTab from '@/components/WorkspacePropsTab';
import WorkspacePropsTabView from '@/components/WorkspacePropsTabView'; import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger'; import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine'; import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
import ModalProcessesList from '@/components/ModalProcessesList';
export default { export default {
name: 'Workspace', name: 'Workspace',
@@ -125,14 +177,18 @@ export default {
WorkspacePropsTab, WorkspacePropsTab,
WorkspacePropsTabView, WorkspacePropsTabView,
WorkspacePropsTabTrigger, WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabScheduler,
ModalProcessesList
}, },
props: { props: {
connection: Object connection: Object
}, },
data () { data () {
return { return {
hasWheelEvent: false hasWheelEvent: false,
isProcessesModal: false
}; };
}, },
computed: { computed: {
@@ -146,15 +202,31 @@ export default {
isSelected () { isSelected () {
return this.selectedWorkspace === this.connection.uid; 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 () { selectedTab () {
if ( if (
this.workspace.breadcrumbs.table === null && (
this.workspace.breadcrumbs.view === null && this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.trigger === null && this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.procedure === null && this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.function === null && this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.scheduler === null && this.workspace.breadcrumbs.function === null &&
['data', 'prop'].includes(this.workspace.selected_tab) 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; return this.queryTabs[0].uid;
@@ -198,7 +270,7 @@ export default {
removeTab: 'workspaces/removeTab' removeTab: 'workspaces/removeTab'
}), }),
addTab () { addTab () {
this.newTab(this.connection.uid); this.newTab({ uid: this.connection.uid });
if (!this.hasWheelEvent) { if (!this.hasWheelEvent) {
this.$refs.tabWrap.addEventListener('wheel', e => { this.$refs.tabWrap.addEventListener('wheel', e => {
@@ -211,6 +283,12 @@ export default {
closeTab (tUid) { closeTab (tUid) {
if (this.queryTabs.length === 1) return; if (this.queryTabs.length === 1) return;
this.removeTab({ uid: this.connection.uid, tab: tUid }); this.removeTab({ uid: this.connection.uid, tab: tUid });
},
showProcessesModal () {
this.isProcessesModal = true;
},
hideProcessesModal () {
this.isProcessesModal = false;
} }
} }
}; };
@@ -226,7 +304,6 @@ export default {
height: calc(100vh - #{$excluding-size}); height: calc(100vh - #{$excluding-size});
.tab-block { .tab-block {
background: $bg-color-light;
margin-top: 0; margin-top: 0;
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
@@ -246,7 +323,6 @@ export default {
> a { > a {
padding: 0.2rem 0.8rem; padding: 0.2rem 0.8rem;
color: $body-font-color;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -275,6 +351,42 @@ export default {
opacity: 1; opacity: 1;
} }
&.tools-dropdown {
.tab-link:focus {
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;
.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 { .workspace-tools-link {
padding-bottom: 0; padding-bottom: 0;
padding-top: 0.3rem; padding-top: 0.3rem;
@@ -294,11 +406,9 @@ export default {
.th { .th {
position: sticky; position: sticky;
top: 0; top: 0;
background: $bg-color;
border: 1px solid; border: 1px solid;
border-left: none; border-left: none;
border-bottom-width: 2px; border-bottom-width: 2px;
border-color: $bg-color-light;
padding: 0; padding: 0;
font-weight: 700; font-weight: 700;
font-size: 0.7rem; font-size: 0.7rem;
@@ -313,7 +423,6 @@ export default {
.td { .td {
border-right: 1px solid; border-right: 1px solid;
border-bottom: 1px solid; border-bottom: 1px solid;
border-color: $bg-color-light;
padding: 0 0.4rem; padding: 0 0.4rem;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 200px; max-width: 200px;
@@ -323,8 +432,6 @@ export default {
position: relative; position: relative;
&:focus { &:focus {
box-shadow: inset 0 0 0 1px $body-font-color;
background: rgba($color: #000, $alpha: 0.3);
outline: none; outline: none;
} }
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="columns"> <div class="columns">
<div class="column col-12 empty text-light"> <div class="column col-12 empty">
<div class="empty-icon"> <div class="empty-icon">
<i class="mdi mdi-48px mdi-power-plug-off" /> <i class="mdi mdi-48px mdi-power-plug-off" />
</div> </div>

View File

@@ -11,7 +11,7 @@
<span v-if="workspace.connected" class="workspace-explorebar-tools"> <span v-if="workspace.connected" class="workspace-explorebar-tools">
<i <i
class="mdi mdi-18px mdi-database-plus c-hand mr-2" class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewDatabase')" :title="$t('message.createNewSchema')"
@click="showNewDBModal" @click="showNewDBModal"
/> />
<i <i
@@ -27,24 +27,35 @@
/> />
</span> </span>
</div> </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 <WorkspaceConnectPanel
v-if="!workspace.connected" v-if="!workspace.connected"
class="workspace-explorebar-body" class="workspace-explorebar-body"
:connection="connection" :connection="connection"
/> />
<div v-else class="workspace-explorebar-body"> <div v-else class="workspace-explorebar-body">
<WorkspaceExploreBarDatabase <WorkspaceExploreBarSchema
v-for="db of workspace.structure" v-for="db of workspace.structure"
:key="db.name" :key="db.name"
:database="db" :database="db"
:connection="connection" :connection="connection"
@show-database-context="openDatabaseContext" @show-schema-context="openSchemaContext"
@show-table-context="openTableContext" @show-table-context="openTableContext"
@show-misc-context="openMiscContext" @show-misc-context="openMiscContext"
/> />
</div> </div>
</div> </div>
<ModalNewDatabase <ModalNewSchema
v-if="isNewDBModal" v-if="isNewDBModal"
@close="hideNewDBModal" @close="hideNewDBModal"
@reload="refresh" @reload="refresh"
@@ -73,6 +84,18 @@
@close="hideCreateRoutineModal" @close="hideCreateRoutineModal"
@open-create-routine-editor="openCreateRoutineEditor" @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 <DatabaseContext
v-if="isDatabaseContext" v-if="isDatabaseContext"
:selected-database="selectedDatabase" :selected-database="selectedDatabase"
@@ -82,6 +105,8 @@
@show-create-view-modal="showCreateViewModal" @show-create-view-modal="showCreateViewModal"
@show-create-trigger-modal="showCreateTriggerModal" @show-create-trigger-modal="showCreateTriggerModal"
@show-create-routine-modal="showCreateRoutineModal" @show-create-routine-modal="showCreateRoutineModal"
@show-create-function-modal="showCreateFunctionModal"
@show-create-scheduler-modal="showCreateSchedulerModal"
@reload="refresh" @reload="refresh"
/> />
<TableContext <TableContext
@@ -103,37 +128,42 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import _ from 'lodash';// TODO: remove
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views'; import Views from '@/ipc-api/Views';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel'; import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase'; import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema';
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext'; import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext';
import TableContext from '@/components/WorkspaceExploreBarTableContext'; import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext'; import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import ModalNewDatabase from '@/components/ModalNewDatabase'; import ModalNewSchema from '@/components/ModalNewSchema';
import ModalNewTable from '@/components/ModalNewTable'; import ModalNewTable from '@/components/ModalNewTable';
import ModalNewView from '@/components/ModalNewView'; import ModalNewView from '@/components/ModalNewView';
import ModalNewTrigger from '@/components/ModalNewTrigger'; import ModalNewTrigger from '@/components/ModalNewTrigger';
import ModalNewRoutine from '@/components/ModalNewRoutine'; import ModalNewRoutine from '@/components/ModalNewRoutine';
import ModalNewFunction from '@/components/ModalNewFunction';
import ModalNewScheduler from '@/components/ModalNewScheduler';
export default { export default {
name: 'WorkspaceExploreBar', name: 'WorkspaceExploreBar',
components: { components: {
WorkspaceConnectPanel, WorkspaceConnectPanel,
WorkspaceExploreBarDatabase, WorkspaceExploreBarSchema,
DatabaseContext, DatabaseContext,
TableContext, TableContext,
MiscContext, MiscContext,
ModalNewDatabase, ModalNewSchema,
ModalNewTable, ModalNewTable,
ModalNewView, ModalNewView,
ModalNewTrigger, ModalNewTrigger,
ModalNewRoutine ModalNewRoutine,
ModalNewFunction,
ModalNewScheduler
}, },
props: { props: {
connection: Object, connection: Object,
@@ -148,8 +178,12 @@ export default {
isNewViewModal: false, isNewViewModal: false,
isNewTriggerModal: false, isNewTriggerModal: false,
isNewRoutineModal: false, isNewRoutineModal: false,
isNewFunctionModal: false,
isNewSchedulerModal: false,
localWidth: null, localWidth: null,
explorebarWidthInterval: null,
searchTermInterval: null,
isDatabaseContext: false, isDatabaseContext: false,
isTableContext: false, isTableContext: false,
isMiscContext: false, isMiscContext: false,
@@ -160,7 +194,8 @@ export default {
selectedDatabase: '', selectedDatabase: '',
selectedTable: null, selectedTable: null,
selectedMisc: null selectedMisc: null,
searchTerm: ''
}; };
}, },
computed: { computed: {
@@ -177,11 +212,22 @@ export default {
} }
}, },
watch: { watch: {
localWidth: _.debounce(function (val) { localWidth (val) {
this.changeExplorebarSize(val); clearTimeout(this.explorebarWidthInterval);
}, 500),
this.explorebarWidthInterval = setTimeout(() => {
this.changeExplorebarSize(val);
}, 500);
},
isSelected (val) { isSelected (val) {
if (val) this.localWidth = this.explorebarSize; if (val) this.localWidth = this.explorebarSize;
},
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => {
this.setSearchTerm(this.searchTerm);
}, 200);
} }
}, },
created () { created () {
@@ -190,7 +236,7 @@ export default {
mounted () { mounted () {
const resizer = this.$refs.resizer; const resizer = this.$refs.resizer;
resizer.addEventListener('mousedown', (e) => { resizer.addEventListener('mousedown', e => {
e.preventDefault(); e.preventDefault();
window.addEventListener('mousemove', this.resize); window.addEventListener('mousemove', this.resize);
@@ -203,6 +249,7 @@ export default {
refreshStructure: 'workspaces/refreshStructure', refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs', changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
selectTab: 'workspaces/selectTab', selectTab: 'workspaces/selectTab',
setSearchTerm: 'workspaces/setSearchTerm',
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
changeExplorebarSize: 'settings/changeExplorebarSize' changeExplorebarSize: 'settings/changeExplorebarSize'
}), }),
@@ -252,8 +299,8 @@ export default {
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
}, },
openDatabaseContext (payload) { openSchemaContext (payload) {
this.selectedDatabase = payload.database; this.selectedDatabase = payload.schema;
this.databaseContextEvent = payload.event; this.databaseContextEvent = payload.event;
this.isDatabaseContext = true; this.isDatabaseContext = true;
}, },
@@ -344,6 +391,52 @@ export default {
} }
else else
this.addNotification({ status: 'error', message: response }); 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 });
} }
} }
}; };
@@ -367,8 +460,6 @@ export default {
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
text-align: left; text-align: left;
background: $bg-color-gray;
box-shadow: 0 0 1px 0 #000;
z-index: 8; z-index: 8;
flex: initial; flex: initial;
position: relative; position: relative;
@@ -409,9 +500,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 { .workspace-explorebar-body {
width: 100%; width: 100%;
height: calc((100vh - 30px) - #{$excluding-size}); height: calc((100vh - 58px) - #{$excluding-size});
overflow: overlay; overflow: overlay;
padding: 0 0.1rem; padding: 0 0.1rem;
} }

View File

@@ -4,9 +4,9 @@
@close-context="closeContext" @close-context="closeContext"
> >
<div <div
v-if="selectedMisc.type === 'procedure'" v-if="['procedure', 'function'].includes(selectedMisc.type)"
class="context-element disabled" class="context-element"
@click="showRunModal" @click="runElementCheck"
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
</div> </div>
@@ -29,6 +29,13 @@
</div> </div>
</div> </div>
</ConfirmModal> </ConfirmModal>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localElement"
:client="workspace.client"
@confirm="runElement"
@close="hideAskParamsModal"
/>
</BaseContextMenu> </BaseContextMenu>
</template> </template>
@@ -36,14 +43,18 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
export default { export default {
name: 'WorkspaceExploreBarMiscContext', name: 'WorkspaceExploreBarMiscContext',
components: { components: {
BaseContextMenu, BaseContextMenu,
ConfirmModal ConfirmModal,
ModalAskParameters
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
@@ -52,7 +63,9 @@ export default {
data () { data () {
return { return {
isDeleteModal: false, isDeleteModal: false,
isRunModal: false isRunModal: false,
isAskingParameters: false,
localElement: {}
}; };
}, },
computed: { computed: {
@@ -69,6 +82,10 @@ export default {
return this.$t('message.deleteTrigger'); return this.$t('message.deleteTrigger');
case 'procedure': case 'procedure':
return this.$t('message.deleteRoutine'); return this.$t('message.deleteRoutine');
case 'function':
return this.$t('message.deleteFunction');
case 'scheduler':
return this.$t('message.deleteScheduler');
default: default:
return ''; return '';
} }
@@ -77,7 +94,8 @@ export default {
methods: { methods: {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs' changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}), }),
showCreateTableModal () { showCreateTableModal () {
this.$emit('show-create-table-modal'); this.$emit('show-create-table-modal');
@@ -88,11 +106,12 @@ export default {
hideDeleteModal () { hideDeleteModal () {
this.isDeleteModal = false; this.isDeleteModal = false;
}, },
showRunModal () { showAskParamsModal () {
this.isRunModal = true; this.isAskingParameters = true;
}, },
hideRunModal () { hideAskParamsModal () {
this.isRunModal = false; this.isAskingParameters = false;
this.closeContext();
}, },
closeContext () { closeContext () {
this.$emit('close-context'); this.$emit('close-context');
@@ -114,12 +133,18 @@ export default {
routine: this.selectedMisc.name routine: this.selectedMisc.name
}); });
break; break;
// case 'schedules': case 'function':
// res = await Tables.dropScheduler({ res = await Functions.dropFunction({
// uid: this.selectedWorkspace, uid: this.selectedWorkspace,
// scheduler: this.selectedMisc.name func: this.selectedMisc.name
// }); });
// break; break;
case 'scheduler':
res = await Schedulers.dropScheduler({
uid: this.selectedWorkspace,
scheduler: this.selectedMisc.name
});
break;
} }
const { status, response } = res; const { status, response } = res;
@@ -136,6 +161,105 @@ export default {
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); 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 <summary
class="accordion-header database-name" class="accordion-header database-name"
:class="{'text-bold': breadcrumbs.schema === database.name}" :class="{'text-bold': breadcrumbs.schema === database.name}"
@click="changeBreadcrumbs({schema: database.name, table: null})" @click="selectSchema(database.name)"
@contextmenu.prevent="showDatabaseContext($event, 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" /> <i class="database-icon mdi mdi-18px mdi-database mr-1" />
<span>{{ database.name }}</span> <span>{{ database.name }}</span>
</summary> </summary>
@@ -14,7 +15,7 @@
<div class="database-tables"> <div class="database-tables">
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="table of database.tables" v-for="table of filteredTables"
:key="table.name" :key="table.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}" :class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@@ -23,7 +24,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" /> <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> </a>
<div <div
v-if="table.type === 'table'" v-if="table.type === 'table'"
@@ -36,7 +37,7 @@
</ul> </ul>
</div> </div>
<div v-if="database.triggers.length" class="database-misc"> <div v-if="filteredTriggers.length && customizations.triggers" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}"> <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" /> <i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
@@ -46,7 +47,7 @@
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="trigger of database.triggers" v-for="trigger of filteredTriggers"
:key="trigger.name" :key="trigger.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@@ -55,7 +56,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-table-cog mdi-18px mr-1" /> <i class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<span>{{ trigger.name }}</span> <span v-html="highlightWord(trigger.name)" />
</a> </a>
</li> </li>
</ul> </ul>
@@ -64,7 +65,7 @@
</details> </details>
</div> </div>
<div v-if="database.procedures.length" class="database-misc"> <div v-if="filteredProcedures.length && customizations.routines" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}"> <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" /> <i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
@@ -74,7 +75,7 @@
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="procedure of database.procedures" v-for="procedure of filteredProcedures"
:key="procedure.name" :key="procedure.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@@ -83,7 +84,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" /> <i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span>{{ procedure.name }}</span> <span v-html="highlightWord(procedure.name)" />
</a> </a>
</li> </li>
</ul> </ul>
@@ -92,7 +93,7 @@
</details> </details>
</div> </div>
<div v-if="database.functions.length" class="database-misc"> <div v-if="filteredFunctions.length && customizations.functions" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}"> <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" /> <i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
@@ -102,7 +103,7 @@
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="func of database.functions" v-for="func of filteredFunctions"
:key="func.name" :key="func.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@@ -111,7 +112,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" /> <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> </a>
</li> </li>
</ul> </ul>
@@ -120,7 +121,7 @@
</details> </details>
</div> </div>
<div v-if="database.schedulers.length" class="database-misc"> <div v-if="filteredSchedulers.length && customizations.schedulers" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}"> <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" /> <i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
@@ -130,7 +131,7 @@
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="scheduler of database.schedulers" v-for="scheduler of filteredSchedulers"
:key="scheduler.name" :key="scheduler.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@@ -139,7 +140,7 @@
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" /> <i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<span>{{ scheduler.name }}</span> <span v-html="highlightWord(scheduler.name)" />
</a> </a>
</li> </li>
</ul> </ul>
@@ -156,18 +157,50 @@ import { mapActions, mapGetters } from 'vuex';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
export default { export default {
name: 'WorkspaceExploreBarDatabase', name: 'WorkspaceExploreBarSchema',
props: { props: {
database: Object, database: Object,
connection: Object connection: Object
}, },
data () {
return {
isLoading: false
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
getWorkspace: 'workspaces/getWorkspace' getLoadedSchemas: 'workspaces/getLoadedSchemas',
getWorkspace: 'workspaces/getWorkspace',
getSearchTerm: 'workspaces/getSearchTerm',
applicationTheme: 'settings/getApplicationTheme'
}), }),
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 () { breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs; return this.getWorkspace(this.connection.uid).breadcrumbs;
}, },
customizations () {
return this.getWorkspace(this.connection.uid).customizations;
},
loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid);
},
maxSize () { maxSize () {
return this.database.tables.reduce((acc, curr) => { return this.database.tables.reduce((acc, curr) => {
if (curr.size > acc) acc = curr.size; if (curr.size > acc) acc = curr.size;
@@ -180,12 +213,22 @@ export default {
}, },
methods: { methods: {
...mapActions({ ...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs' changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
refreshSchema: 'workspaces/refreshSchema'
}), }),
formatBytes, formatBytes,
showDatabaseContext (event, database) { async selectSchema (schema) {
this.changeBreadcrumbs({ schema: database, table: null }); if (!this.loadedSchemas.has(schema)) {
this.$emit('show-database-context', { event, database }); 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) { showTableContext (event, table) {
this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name }); this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name });
@@ -197,11 +240,22 @@ export default {
}, },
piePercentage (val) { piePercentage (val) {
const perc = val / this.maxSize * 100; const perc = val / this.maxSize * 100;
return { background: `conic-gradient(lime ${perc}%, white 0)` }; if (this.applicationTheme === 'dark')
return { background: `conic-gradient(lime ${perc}%, white 0)` };
else
return { background: `conic-gradient(teal ${perc}%, silver 0)` };
}, },
setBreadcrumbs (payload) { setBreadcrumbs (payload) {
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return; if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
this.changeBreadcrumbs(payload); 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 +263,12 @@ export default {
<style lang="scss"> <style lang="scss">
.workspace-explorebar-database { .workspace-explorebar-database {
.database-name {
position: sticky;
top: 0;
z-index: 2;
}
.database-name, .database-name,
.misc-name, .misc-name,
a.table-name { a.table-name {
@@ -230,6 +290,16 @@ export default {
.misc-icon { .misc-icon {
opacity: 0.7; opacity: 0.7;
} }
.loading {
height: 18px;
width: 18px;
&::after {
height: 0.6rem;
width: 0.6rem;
}
}
} }
.misc-name { .misc-name {
@@ -241,26 +311,15 @@ export default {
.database-name, .database-name,
.misc-name { .misc-name {
&:hover { &:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px; border-radius: 2px;
} }
} }
a.table-name {
&:hover {
color: inherit;
background: inherit;
}
}
.menu-item { .menu-item {
line-height: 1.2; line-height: 1.2;
position: relative; position: relative;
&:hover { &:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px; 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> <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" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu"> <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> <span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div> </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> <span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
</div> </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> <span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div> </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> <span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div> </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> <span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div> </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> <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>
</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> <span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div class="context-element" @click="showDeleteModal">
@@ -36,12 +64,12 @@
<ConfirmModal <ConfirmModal
v-if="isDeleteModal" v-if="isDeleteModal"
@confirm="deleteDatabase" @confirm="deleteSchema"
@hide="hideDeleteModal" @hide="hideDeleteModal"
> >
<template slot="header"> <template slot="header">
<div class="d-flex"> <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> </div>
</template> </template>
<div slot="body"> <div slot="body">
@@ -50,7 +78,7 @@
</div> </div>
</div> </div>
</ConfirmModal> </ConfirmModal>
<ModalEditDatabase <ModalEditSchema
v-if="isEditModal" v-if="isEditModal"
:selected-database="selectedDatabase" :selected-database="selectedDatabase"
@close="hideEditModal" @close="hideEditModal"
@@ -62,15 +90,15 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditDatabase from '@/components/ModalEditDatabase'; import ModalEditSchema from '@/components/ModalEditSchema';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
export default { export default {
name: 'WorkspaceExploreBarDatabaseContext', name: 'WorkspaceExploreBarSchemaContext',
components: { components: {
BaseContextMenu, BaseContextMenu,
ConfirmModal, ConfirmModal,
ModalEditDatabase ModalEditSchema
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
@@ -108,6 +136,12 @@ export default {
showCreateRoutineModal () { showCreateRoutineModal () {
this.$emit('show-create-routine-modal'); this.$emit('show-create-routine-modal');
}, },
showCreateFunctionModal () {
this.$emit('show-create-function-modal');
},
showCreateSchedulerModal () {
this.$emit('show-create-scheduler-modal');
},
showDeleteModal () { showDeleteModal () {
this.isDeleteModal = true; this.isDeleteModal = true;
}, },
@@ -124,9 +158,9 @@ export default {
closeContext () { closeContext () {
this.$emit('close-context'); this.$emit('close-context');
}, },
async deleteDatabase () { async deleteSchema () {
try { try {
const { status, response } = await Database.deleteDatabase({ const { status, response } = await Schema.deleteSchema({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
database: this.selectedDatabase database: this.selectedDatabase
}); });

View File

@@ -2,6 +2,7 @@
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="$t('word.confirm')"
size="medium" size="medium"
class="options-modal"
@confirm="confirmForeignsChange" @confirm="confirmForeignsChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
@@ -36,7 +37,7 @@
v-for="foreign in foreignProxy" v-for="foreign in foreignProxy"
:key="foreign._id" :key="foreign._id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-foreign': selectedForeignID === foreign._id}" :class="{'selected-element': selectedForeignID === foreign._id}"
@click="selectForeign($event, foreign._id)" @click="selectForeign($event, foreign._id)"
> >
<div class="tile-icon"> <div class="tile-icon">
@@ -268,8 +269,10 @@ export default {
this.$emit('foreigns-update', this.foreignProxy); this.$emit('foreigns-update', this.foreignProxy);
}, },
selectForeign (event, id) { selectForeign (event, id) {
if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field')) if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field')) {
this.selectedForeignID = id; this.selectedForeignID = id;
this.getRefFields();
}
}, },
getModalInnerHeight () { getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
@@ -369,23 +372,16 @@ export default {
} }
&:hover { &:hover {
background: $bg-color-light;
.tile-action { .tile-action {
opacity: 1; opacity: 1;
} }
} }
&.selected-foreign { &.selected-element {
background: $bg-color-light;
opacity: 1; opacity: 1;
} }
} }
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list { .fields-list {
max-height: 80px; max-height: 80px;
overflow: auto; overflow: auto;

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,271 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
class="options-modal"
@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-element': 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 {
.tile-action {
opacity: 1;
}
}
&.selected-element {
opacity: 1;
}
}
.fields-list {
max-height: 300px;
overflow: auto;
}
.remove-field .mdi {
pointer-events: none;
}
</style>

View File

@@ -2,6 +2,7 @@
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="$t('word.confirm')"
size="medium" size="medium"
class="options-modal"
@confirm="confirmIndexesChange" @confirm="confirmIndexesChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
@@ -36,7 +37,7 @@
v-for="index in indexesProxy" v-for="index in indexesProxy"
:key="index._id" :key="index._id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-index': selectedIndexID === index._id}" :class="{'selected-element': selectedIndexID === index._id}"
@click="selectIndex($event, index._id)" @click="selectIndex($event, index._id)"
> >
<div class="tile-icon"> <div class="tile-icon">
@@ -256,23 +257,16 @@ export default {
} }
&:hover { &:hover {
background: $bg-color-light;
.tile-action { .tile-action {
opacity: 1; opacity: 1;
} }
} }
&.selected-index { &.selected-element {
background: $bg-color-light;
opacity: 1; opacity: 1;
} }
} }
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list { .fields-list {
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;

View File

@@ -26,7 +26,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -38,7 +38,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.autoIncrement" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.autoIncrement') }} {{ $t('word.autoIncrement') }}
</label> </label>
@@ -50,7 +50,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.collation') }} {{ $t('word.collation') }}
</label> </label>
@@ -66,7 +66,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.engine') }} {{ $t('word.engine') }}
</label> </label>

View File

@@ -26,7 +26,7 @@
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.definer" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.definer') }} {{ $t('word.definer') }}
</label> </label>
@@ -54,7 +54,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</label> </label>
@@ -77,7 +77,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureDataAccess" class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.dataAccess') }} {{ $t('message.dataAccess') }}
</label> </label>
@@ -90,7 +90,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" /> <div class="col-4" />
<div class="column"> <div class="column">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
@@ -124,6 +124,9 @@ export default {
computed: { computed: {
isTableNameValid () { isTableNameValid () {
return this.optionsProxy.name !== ''; return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
} }
}, },
created () { created () {

View File

@@ -2,7 +2,8 @@
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="$t('word.confirm')"
size="medium" size="medium"
@confirm="confirmIndexesChange" class="options-modal"
@confirm="confirmParametersChange"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
<template :slot="'header'"> <template :slot="'header'">
@@ -34,14 +35,14 @@
<div ref="parametersPanel" class="panel-body p-0 pr-1"> <div ref="parametersPanel" class="panel-body p-0 pr-1">
<div <div
v-for="param in parametersProxy" v-for="param in parametersProxy"
:key="param.name" :key="param._id"
class="tile tile-centered c-hand mb-1 p-1" class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}" :class="{'selected-element': selectedParam === param._id}"
@click="selectParameter($event, param.name)" @click="selectParameter($event, param._id)"
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <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> </div>
<div class="tile-content"> <div class="tile-content">
@@ -54,7 +55,7 @@
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="$t('word.delete')"
@click.prevent="removeParameter(param.name)" @click.prevent="removeParameter(param._id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
</button> </button>
@@ -105,7 +106,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.length') }} {{ $t('word.length') }}
</label> </label>
@@ -114,6 +115,7 @@
v-model="selectedParamObj.length" v-model="selectedParamObj.length"
class="form-input" class="form-input"
type="number" type="number"
min="0"
> >
</div> </div>
</div> </div>
@@ -169,6 +171,7 @@
</template> </template>
<script> <script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
export default { export default {
@@ -192,10 +195,13 @@ export default {
}, },
computed: { computed: {
selectedParamObj () { selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam); return this.parametersProxy.find(param => param._id === this.selectedParam);
}, },
isChanged () { isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy); return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
} }
}, },
mounted () { mounted () {
@@ -212,12 +218,17 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight); window.removeEventListener('resize', this.getModalInnerHeight);
}, },
methods: { methods: {
confirmIndexesChange () { typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy); this.$emit('parameters-update', this.parametersProxy);
}, },
selectParameter (event, name) { selectParameter (event, uid) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field')) if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = name; this.selectedParam = uid;
}, },
getModalInnerHeight () { getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
@@ -226,10 +237,11 @@ export default {
}, },
addParameter () { addParameter () {
this.parametersProxy = [...this.parametersProxy, { this.parametersProxy = [...this.parametersProxy, {
name: `Param${this.i++}`, _id: uidGen(),
type: 'INT', name: `param${this.i++}`,
type: this.workspace.dataTypes[0].types[0].name,
context: 'IN', context: 'IN',
length: 10 length: ''
}]; }];
if (this.parametersProxy.length === 1) if (this.parametersProxy.length === 1)
@@ -239,8 +251,8 @@ export default {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60; this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20); }, 20);
}, },
removeParameter (name) { removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name); this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length) if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID(); this.resetSelectedID();
@@ -253,7 +265,7 @@ export default {
this.resetSelectedID(); this.resetSelectedID();
}, },
resetSelectedID () { resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : ''; this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
} }
} }
}; };
@@ -272,23 +284,16 @@ export default {
} }
&:hover { &:hover {
background: $bg-color-light;
.tile-action { .tile-action {
opacity: 1; opacity: 1;
} }
} }
&.selected-param { &.selected-element {
background: $bg-color-light;
opacity: 1; opacity: 1;
} }
} }
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list { .fields-list {
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;

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>
</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 <WorkspacePropsTable
v-if="localFields" v-if="localFields"
ref="indexTable" ref="indexTable"
@@ -106,6 +107,7 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsTable from '@/components/WorkspacePropsTable'; import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal'; import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal'; import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
@@ -114,6 +116,7 @@ import WorkspacePropsForeignModal from '@/components/WorkspacePropsForeignModal'
export default { export default {
name: 'WorkspacePropsTab', name: 'WorkspacePropsTab',
components: { components: {
BaseLoader,
WorkspacePropsTable, WorkspacePropsTable,
WorkspacePropsOptionsModal, WorkspacePropsOptionsModal,
WorkspacePropsIndexesModal, WorkspacePropsIndexesModal,
@@ -126,7 +129,7 @@ export default {
data () { data () {
return { return {
tabUid: 'prop', tabUid: 'prop',
isQuering: false, isLoading: false,
isSaving: false, isSaving: false,
isOptionsModal: false, isOptionsModal: false,
isIndexesModal: false, isIndexesModal: false,
@@ -205,8 +208,10 @@ export default {
}), }),
async getFieldsData () { async getFieldsData () {
if (!this.table) return; if (!this.table) return;
this.localFields = [];
this.newFieldsCounter = 0; this.newFieldsCounter = 0;
this.isQuering = true; this.isLoading = true;
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
const params = { const params = {
@@ -281,7 +286,7 @@ export default {
this.addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
} }
this.isQuering = false; this.isLoading = false;
}, },
async saveChanges () { async saveChanges () {
if (this.isSaving) return; if (this.isSaving) return;
@@ -428,11 +433,11 @@ export default {
_id: uidGen(), _id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`, name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '', key: '',
type: 'int', type: this.workspace.dataTypes[0].types[0].name,
schema: this.schema, schema: this.schema,
table: this.table, table: this.table,
numPrecision: null, numPrecision: null,
numLength: null, numLength: this.workspace.dataTypes[0].types[0].length,
datePrecision: null, datePrecision: null,
charLength: null, charLength: null,
nullable: false, nullable: 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"
: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"
:client="workspace.client"
@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" /> <div class="divider-vert py-3" />
<button <button
class="btn btn-dark btn-sm disabled" class="btn btn-dark btn-sm"
:disabled="isChanged" :disabled="isChanged"
@click="false" @click="runRoutineCheck"
> >
<span>{{ $t('word.run') }}</span> <span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" /> <i class="mdi mdi-24px mdi-play ml-1" />
@@ -43,10 +43,12 @@
</div> </div>
</div> </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> <label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor <QueryEditor
v-if="isSelected" v-show="isSelected"
:key="`${routine}-${_uid}`"
ref="queryEditor" ref="queryEditor"
:value.sync="localRoutine.sql" :value.sync="localRoutine.sql"
:workspace="workspace" :workspace="workspace"
@@ -69,22 +71,34 @@
@hide="hideParamsModal" @hide="hideParamsModal"
@parameters-update="parametersUpdate" @parameters-update="parametersUpdate"
/> />
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localRoutine"
:client="workspace.client"
@confirm="runRoutine"
@close="hideAskParamsModal"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal'; import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal'; import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
export default { export default {
name: 'WorkspacePropsTabRoutine', name: 'WorkspacePropsTabRoutine',
components: { components: {
QueryEditor, QueryEditor,
BaseLoader,
WorkspacePropsRoutineOptionsModal, WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal WorkspacePropsRoutineParamsModal,
ModalAskParameters
}, },
props: { props: {
connection: Object, connection: Object,
@@ -93,10 +107,11 @@ export default {
data () { data () {
return { return {
tabUid: 'prop', tabUid: 'prop',
isQuering: false, isLoading: false,
isSaving: false, isSaving: false,
isOptionsModal: false, isOptionsModal: false,
isParamsModal: false, isParamsModal: false,
isAskingParameters: false,
originalRoutine: null, originalRoutine: null,
localRoutine: { sql: '' }, localRoutine: { sql: '' },
lastRoutine: null, lastRoutine: null,
@@ -162,11 +177,13 @@ export default {
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure', refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges', setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs' changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}), }),
async getRoutineData () { async getRoutineData () {
if (!this.routine) return; if (!this.routine) return;
this.isQuering = true; this.localRoutine = { sql: '' };
this.isLoading = true;
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
@@ -178,6 +195,12 @@ export default {
const { status, response } = await Routines.getRoutineInformations(params); const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') { if (status === 'success') {
this.originalRoutine = response; 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.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.sqlProxy = this.localRoutine.sql; this.sqlProxy = this.localRoutine.sql;
} }
@@ -189,7 +212,7 @@ export default {
} }
this.resizeQueryEditor(); this.resizeQueryEditor();
this.isQuering = false; this.isLoading = false;
}, },
async saveChanges () { async saveChanges () {
if (this.isSaving) return; if (this.isSaving) return;
@@ -245,6 +268,31 @@ export default {
parametersUpdate (parameters) { parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, 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 () { showOptionsModal () {
this.isOptionsModal = true; this.isOptionsModal = true;
}, },
@@ -256,6 +304,12 @@ export default {
}, },
hideParamsModal () { hideParamsModal () {
this.isParamsModal = false; 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>
<div class="container"> <div class="container">
<div class="columns mb-4"> <div class="columns mb-4">
<div class="column col-3"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ $t('word.name') }}</label>
<input <input
@@ -36,7 +36,7 @@
> >
</div> </div>
</div> </div>
<div class="column col-3"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ $t('word.definer') }}</label>
<select <select
@@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="columns"> <div class="columns">
<div class="column col-3"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label> <label class="form-label">{{ $t('word.table') }}</label>
<select v-model="localTrigger.table" class="form-select"> <select v-model="localTrigger.table" class="form-select">
@@ -77,7 +77,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="column col-3"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label> <label class="form-label">{{ $t('word.event') }}</label>
<div class="input-group"> <div class="input-group">
@@ -95,10 +95,11 @@
</div> </div>
</div> </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> <label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor <QueryEditor
v-if="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
:value.sync="localTrigger.sql" :value.sync="localTrigger.sql"
:workspace="workspace" :workspace="workspace"
@@ -110,13 +111,15 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
export default { export default {
name: 'WorkspacePropsTabTrigger', name: 'WorkspacePropsTabTrigger',
components: { components: {
BaseLoader,
QueryEditor QueryEditor
}, },
props: { props: {
@@ -126,7 +129,7 @@ export default {
data () { data () {
return { return {
tabUid: 'prop', tabUid: 'prop',
isQuering: false, isLoading: false,
isSaving: false, isSaving: false,
originalTrigger: null, originalTrigger: null,
localTrigger: { sql: '' }, localTrigger: { sql: '' },
@@ -197,7 +200,9 @@ export default {
}), }),
async getTriggerData () { async getTriggerData () {
if (!this.trigger) return; if (!this.trigger) return;
this.isQuering = true;
this.localTrigger = { sql: '' };
this.isLoading = true;
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
@@ -220,7 +225,7 @@ export default {
} }
this.resizeQueryEditor(); this.resizeQueryEditor();
this.isQuering = false; this.isLoading = false;
}, },
async saveChanges () { async saveChanges () {
if (this.isSaving) return; if (this.isSaving) return;

View File

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

View File

@@ -42,6 +42,13 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="customizations.tableArray" class="th">
<div class="column-resizable">
<div class="table-column-title">
{{ $t('word.array') }}
</div>
</div>
</div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
@@ -49,21 +56,21 @@
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.unsigned" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.unsigned') }} {{ $t('word.unsigned') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.nullable" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('message.allowNull') }} {{ $t('message.allowNull') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.zerofill" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('message.zeroFill') }} {{ $t('message.zeroFill') }}
@@ -77,14 +84,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.comment" class="th">
<div class="column-resizable"> <div class="column-resizable">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.comment') }} {{ $t('word.comment') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div v-if="customizations.collation" class="th">
<div class="column-resizable min-100"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.collation') }} {{ $t('word.collation') }}
@@ -106,6 +113,7 @@
:indexes="getIndexes(row.name)" :indexes="getIndexes(row.name)"
:foreigns="getForeigns(row.name)" :foreigns="getForeigns(row.name)"
:data-types="dataTypes" :data-types="dataTypes"
:customizations="customizations"
@contextmenu="contextMenu" @contextmenu="contextMenu"
/> />
</draggable> </draggable>
@@ -154,6 +162,9 @@ export default {
workspaceSchema () { workspaceSchema () {
return this.getWorkspace(this.connUid).breadcrumbs.schema; return this.getWorkspace(this.connUid).breadcrumbs.schema;
}, },
customizations () {
return this.getWorkspace(this.connUid).customizations;
},
dataTypes () { dataTypes () {
return this.getWorkspace(this.connUid).dataTypes; return this.getWorkspace(this.connUid).dataTypes;
}, },

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)"> <div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)">
<div class="td" tabindex="0"> <div class="td" tabindex="0">
<div class="row-draggable"> <div :class="customizations.sortableFields ? 'row-draggable' : 'text-center'">
<i class="mdi mdi-drag-horizontal row-draggable-icon" /> <i v-if="customizations.sortableFields" class="mdi mdi-drag-horizontal row-draggable-icon" />
{{ localRow.order }} {{ localRow.order }}
</div> </div>
</div> </div>
@@ -48,7 +48,7 @@
<span <span
v-if="!isInlineEditor.type" v-if="!isInlineEditor.type"
class="cell-content text-left" class="cell-content text-left"
:class="`type-${lowerCase(localRow.type)}`" :class="typeClass(localRow.type)"
@click="editON($event, localRow.type.toUpperCase(), 'type')" @click="editON($event, localRow.type.toUpperCase(), 'type')"
> >
{{ localRow.type }} {{ localRow.type }}
@@ -76,6 +76,16 @@
</optgroup> </optgroup>
</select> </select>
</div> </div>
<div
v-if="customizations.tableArray"
class="td"
tabindex="0"
>
<label class="form-checkbox">
<input v-model="localRow.isArray" type="checkbox">
<i class="form-icon" />
</label>
</div>
<div class="td type-int" tabindex="0"> <div class="td type-int" tabindex="0">
<template v-if="fieldType.length"> <template v-if="fieldType.length">
<span <span
@@ -96,7 +106,11 @@
> >
</template> </template>
</div> </div>
<div class="td" tabindex="0"> <div
v-if="customizations.unsigned"
class="td"
tabindex="0"
>
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.unsigned" v-model="localRow.unsigned"
@@ -106,7 +120,11 @@
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td" tabindex="0"> <div
v-if="customizations.nullable"
class="td"
tabindex="0"
>
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.nullable" v-model="localRow.nullable"
@@ -116,7 +134,11 @@
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td" tabindex="0"> <div
v-if="customizations.zerofill"
class="td"
tabindex="0"
>
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.zerofill" v-model="localRow.zerofill"
@@ -131,7 +153,11 @@
{{ fieldDefault }} {{ fieldDefault }}
</span> </span>
</div> </div>
<div class="td type-varchar" tabindex="0"> <div
v-if="customizations.comment"
class="td type-varchar"
tabindex="0"
>
<span <span
v-if="!isInlineEditor.comment" v-if="!isInlineEditor.comment"
class="cell-content" class="cell-content"
@@ -149,7 +175,11 @@
@blur="editOFF" @blur="editOFF"
> >
</div> </div>
<div class="td" tabindex="0"> <div
v-if="customizations.collation"
class="td"
tabindex="0"
>
<template v-if="fieldType.collation"> <template v-if="fieldType.collation">
<span <span
v-if="!isInlineEditor.collation" v-if="!isInlineEditor.collation"
@@ -220,7 +250,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mb-2"> <div v-if="customizations.nullable" class="mb-2">
<label class="form-radio form-inline"> <label class="form-radio form-inline">
<input <input
v-model="defaultValue.type" v-model="defaultValue.type"
@@ -230,7 +260,7 @@
><i class="form-icon" /> NULL ><i class="form-icon" /> NULL
</label> </label>
</div> </div>
<div class="mb-2"> <div v-if="customizations.autoIncrement" class="mb-2">
<label class="form-radio form-inline"> <label class="form-radio form-inline">
<input <input
v-model="defaultValue.type" v-model="defaultValue.type"
@@ -261,7 +291,7 @@
</div> </div>
</div> </div>
</div> </div>
<div> <div v-if="customizations.onUpdate">
<div class="form-group"> <div class="form-group">
<label class="form-label col-4"> <label class="form-label col-4">
{{ $t('message.onUpdate') }} {{ $t('message.onUpdate') }}
@@ -294,7 +324,8 @@ export default {
row: Object, row: Object,
dataTypes: Array, dataTypes: Array,
indexes: Array, indexes: Array,
foreigns: Array foreigns: Array,
customizations: Object
}, },
data () { data () {
return { return {
@@ -318,7 +349,7 @@ export default {
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
localLength () { 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 () { fieldType () {
const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type => const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type =>
@@ -378,10 +409,10 @@ export default {
return 'UNKNOWN ' + key; return 'UNKNOWN ' + key;
} }
}, },
lowerCase (val) { typeClass (type) {
if (val) if (type)
return val.toLowerCase(); return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return val; return '';
}, },
initLocalRow () { initLocalRow () {
Object.keys(this.localRow).forEach(key => { Object.keys(this.localRow).forEach(key => {
@@ -400,7 +431,7 @@ export default {
this.defaultValue.type = 'custom'; this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default.replace(/(^')|('$)/g, ''); 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.type = 'custom';
this.defaultValue.custom = this.localRow.default; 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 v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12"> <div class="workspace-query-runner column col-12">
<QueryEditor <QueryEditor
v-if="isSelected" v-show="isSelected"
ref="queryEditor"
:auto-focus="true" :auto-focus="true"
:value.sync="query" :value.sync="query"
:workspace="workspace" :workspace="workspace"
:schema="schema" :schema="schema"
:is-selected="isSelected"
:height="editorHeight"
/> />
<div ref="resizer" class="query-area-resizer" />
<div class="workspace-query-runner-footer"> <div class="workspace-query-runner-footer">
<div class="workspace-query-buttons"> <div class="workspace-query-buttons">
<button <button
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
:class="{'loading':isQuering}" :class="{'loading':isQuering}"
:disabled="!query" :disabled="!query"
title="F9" title="F5"
@click="runQuery(query)" @click="runQuery(query)"
> >
<span>{{ $t('word.run') }}</span> <span>{{ $t('word.run') }}</span>
@@ -22,6 +26,13 @@
</button> </button>
</div> </div>
<div class="workspace-query-info"> <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"> <div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b> {{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
</div> </div>
@@ -38,14 +49,16 @@
</div> </div>
</div> </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 <WorkspaceQueryTable
v-if="results" v-if="results"
v-show="!isQuering" v-show="!isQuering"
ref="queryTable" ref="queryTable"
:results="results" :results="results"
:tab-uid="tabUid" :tab-uid="tab.uid"
:conn-uid="connection.uid" :conn-uid="connection.uid"
:is-selected="isSelected"
mode="query" mode="query"
@update-field="updateField" @update-field="updateField"
@delete-selected="deleteSelected" @delete-selected="deleteSelected"
@@ -55,8 +68,9 @@
</template> </template>
<script> <script>
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable'; import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs'; import tableTabs from '@/mixins/tableTabs';
@@ -64,13 +78,14 @@ import tableTabs from '@/mixins/tableTabs';
export default { export default {
name: 'WorkspaceQueryTab', name: 'WorkspaceQueryTab',
components: { components: {
BaseLoader,
QueryEditor, QueryEditor,
WorkspaceQueryTable WorkspaceQueryTable
}, },
mixins: [tableTabs], mixins: [tableTabs],
props: { props: {
connection: Object, connection: Object,
tabUid: String, tab: Object,
isSelected: Boolean isSelected: Boolean
}, },
data () { data () {
@@ -80,20 +95,41 @@ export default {
isQuering: false, isQuering: false,
results: [], results: [],
resultsCount: 0, resultsCount: 0,
affectedCount: 0 durationsCount: 0,
affectedCount: 0,
editorHeight: 200
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
},
isWorkspaceSelected () {
return this.workspace.uid === this.selectedWorkspace;
} }
}, },
created () { created () {
this.query = this.tab.content;
window.addEventListener('keydown', this.onKey); 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 () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
@@ -114,11 +150,12 @@ export default {
query query
}; };
const { status, response } = await Database.rawQuery(params); const { status, response } = await Schema.rawQuery(params);
if (status === 'success') { if (status === 'success') {
this.results = Array.isArray(response) ? response : [response]; this.results = Array.isArray(response) ? response : [response];
this.resultsCount += this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0); 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); this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0);
} }
else else
@@ -137,12 +174,28 @@ export default {
clearTabData () { clearTabData () {
this.results = []; this.results = [];
this.resultsCount = 0; this.resultsCount = 0;
this.durationsCount = 0;
this.affectedCount = 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) { onKey (e) {
if (this.isSelected) { if (this.isSelected && this.isWorkspaceSelected) {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'F9') if (e.key === 'F5')
this.runQuery(this.query); this.runQuery(this.query);
} }
} }
@@ -155,11 +208,23 @@ export default {
align-content: baseline; align-content: baseline;
.workspace-query-runner { .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 { .workspace-query-runner-footer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem; padding: 0.3rem 0.6rem 0.4rem;
align-items: center; align-items: center;
height: 42px;
.workspace-query-buttons { .workspace-query-buttons {
display: flex; display: flex;
@@ -167,7 +232,6 @@ export default {
.btn { .btn {
display: flex; display: flex;
align-self: center; align-self: center;
color: $body-font-color;
margin-right: 0.4rem; margin-right: 0.4rem;
} }
} }
@@ -181,6 +245,10 @@ export default {
} }
} }
} }
.workspace-query-results {
min-height: 200px;
}
} }
</style> </style>

View File

@@ -9,6 +9,7 @@
:context-event="contextEvent" :context-event="contextEvent"
:selected-rows="selectedRows" :selected-rows="selectedRows"
@delete-selected="deleteSelected" @delete-selected="deleteSelected"
@set-null="setNull"
@close-context="isContext = false" @close-context="isContext = false"
/> />
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs"> <ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
@@ -41,7 +42,7 @@
/> />
<span>{{ field.alias || field.name }}</span> <span>{{ field.alias || field.name }}</span>
<i <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="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'" :class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/> />
@@ -64,12 +65,11 @@
v-for="row in items" v-for="row in items"
:key="row._id" :key="row._id"
:row="row" :row="row"
:fields="fields" :fields="fieldsObj"
:key-usage="keyUsage" :key-usage="keyUsage"
class="tr"
:class="{'selected': selectedRows.includes(row._id)}" :class="{'selected': selectedRows.includes(row._id)}"
@select-row="selectRow($event, row._id)" @select-row="selectRow($event, row._id)"
@update-field="updateField($event, getPrimaryValue(row))" @update-field="updateField($event, row)"
@contextmenu="contextMenu" @contextmenu="contextMenu"
/> />
</template> </template>
@@ -80,11 +80,13 @@
<script> <script>
import { uidGen } from 'common/libs/uidGen'; 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 BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow'; import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext'; import TableContext from '@/components/WorkspaceQueryTableContext';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
export default { export default {
name: 'WorkspaceQueryTable', name: 'WorkspaceQueryTable',
@@ -95,9 +97,9 @@ export default {
}, },
props: { props: {
results: Array, results: Array,
tabUid: [String, Number],
connUid: String, connUid: String,
mode: String mode: String,
isSelected: Boolean
}, },
data () { data () {
return { return {
@@ -121,7 +123,16 @@ export default {
return this.getWorkspace(this.connUid).breadcrumbs.schema; return this.getWorkspace(this.connUid).breadcrumbs.schema;
}, },
primaryField () { primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false; const primaryFields = this.fields.filter(field => field.key === 'pri');
const uniqueFields = this.fields.filter(field => field.key === 'uni');
if ((primaryFields.length > 1 || !primaryFields.length) && (uniqueFields.length > 1 || !uniqueFields.length))
return false;
return primaryFields[0] || uniqueFields[0];
},
isSortable () {
return this.fields.every(field => field.name);
}, },
isHardSort () { isHardSort () {
return this.mode === 'table' && this.localResults.length === 1000; return this.mode === 'table' && this.localResults.length === 1000;
@@ -149,6 +160,37 @@ export default {
}, },
keyUsage () { keyUsage () {
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : []; 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: { watch: {
@@ -195,7 +237,8 @@ export default {
}, },
fieldLength (field) { fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null; 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) { keyName (key) {
switch (key) { switch (key) {
@@ -238,7 +281,7 @@ export default {
: []; : [];
}, },
resizeResults () { resizeResults () {
if (this.$refs.resultTable) { if (this.$refs.resultTable && this.isSelected) {
const el = this.$refs.tableWrapper; const el = this.$refs.tableWrapper;
if (el) { if (el) {
@@ -252,37 +295,62 @@ export default {
refreshScroller () { refreshScroller () {
this.resizeResults(); this.resizeResults();
}, },
updateField (payload, id) { updateField (payload, row) {
if (!this.primaryField) const orgRow = this.localResults.find(lr => lr._id === row._id);
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') }); delete row._id;
else { delete orgRow._id;
const params = {
primary: this.primaryField.name, Object.keys(orgRow).forEach(key => { // remap the row
schema: this.getSchema(this.resultsetIndex), if (orgRow[key] instanceof Date && moment(orgRow[key]).isValid()) { // if datetime
table: this.getTable(this.resultsetIndex), let datePrecision = '';
id, const precision = this.fields.find(field => field.name === key).datePrecision;
...payload for (let i = 0; i < precision; i++)
}; datePrecision += i === 0 ? '.S' : 'S';
this.$emit('update-field', params);
} orgRow[key] = moment(orgRow[key]).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
});
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 () { deleteSelected () {
if (!this.primaryField) const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => {
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') }); delete row._id;
else { return row;
const rowIDs = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => });
row[this.primaryField.name] ||
row[`${this.primaryField.table}.${this.primaryField.name}`] || const params = {
row[`${this.primaryField.tableAlias}.${this.primaryField.name}`] primary: this.primaryField.name,
); schema: this.getSchema(this.resultsetIndex),
const params = { table: this.getTable(this.resultsetIndex),
primary: this.primaryField.name, rows
schema: this.getSchema(this.resultsetIndex), };
table: this.getTable(this.resultsetIndex), this.$emit('delete-selected', params);
rows: rowIDs },
}; setNull () {
this.$emit('delete-selected', params); 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) { applyUpdate (params) {
const { primary, id, field, table, content } = params; const { primary, id, field, table, content } = params;
@@ -324,6 +392,8 @@ export default {
this.selectedRows = [row]; this.selectedRows = [row];
}, },
contextMenu (event, cell) { contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.selectedCell = cell; this.selectedCell = cell;
if (!this.selectedRows.includes(cell.id)) if (!this.selectedRows.includes(cell.id))
this.selectedRows = [cell.id]; this.selectedRows = [cell.id];
@@ -331,6 +401,8 @@ export default {
this.isContext = true; this.isContext = true;
}, },
sort (field) { sort (field) {
if (!this.isSortable) return;
if (this.mode === 'query') if (this.mode === 'query')
field = `${this.getTable(this.resultsetIndex)}.${field}`; field = `${this.getTable(this.resultsetIndex)}.${field}`;
@@ -354,6 +426,20 @@ export default {
}, },
selectResultset (index) { selectResultset (index) {
this.resultsetIndex = 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" :context-event="contextEvent"
@close-context="closeContext" @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"> <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> </div>
<ConfirmModal <ConfirmModal
@@ -60,6 +71,10 @@ export default {
deleteRows () { deleteRows () {
this.$emit('delete-selected'); this.$emit('delete-selected');
this.closeContext(); this.closeContext();
},
setNull () {
this.$emit('set-null');
this.closeContext();
} }
} }
}; };

View File

@@ -6,17 +6,17 @@
:key="cKey" :key="cKey"
class="td p-0" class="td p-0"
tabindex="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'"> <template v-if="cKey !== '_id'">
<span <span
v-if="!isInlineEditor[cKey]" v-if="!isInlineEditor[cKey]"
class="cell-content px-2" class="cell-content px-2"
:class="`${isNull(col)} type-${getFieldType(cKey)}`" :class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON($event, col, cKey)" @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 <ForeignKeySelect
v-else-if="foreignKeys.includes(cKey)" v-else-if="isForeignKey(cKey)"
class="editable-field" class="editable-field"
:value.sync="editingContent" :value.sync="editingContent"
:key-usage="getKeyUsage(cKey)" :key-usage="getKeyUsage(cKey)"
@@ -34,6 +34,15 @@
class="editable-field px-2" class="editable-field px-2"
@blur="editOFF" @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 <input
v-else v-else
ref="editField" ref="editField"
@@ -61,14 +70,52 @@
<div :slot="'body'"> <div :slot="'body'">
<div class="mb-2"> <div class="mb-2">
<div> <div>
<textarea <TextEditor
v-model="editingContent" :value.sync="editingContent"
class="form-input textarea-editor" editor-class="textarea-editor"
:mode="editorMode"
/> />
</div> </div>
<div class="editor-field-info"> <div class="editor-field-info p-vcentered">
<div><b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}</div> <div class="d-flex p-vcentered">
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div> <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> </div>
</div> </div>
@@ -135,19 +182,21 @@ import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64'; import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT, HAS_TIMEZONE } from 'common/fieldTypes';
import { mask } from 'vue-the-mask'; import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect';
export default { export default {
name: 'WorkspaceQueryTableRow', name: 'WorkspaceQueryTableRow',
components: { components: {
ConfirmModal, ConfirmModal,
TextEditor,
ForeignKeySelect ForeignKeySelect
}, },
directives: { directives: {
mask mask: VueMaskDirective
}, },
filters: { filters: {
formatBytes, formatBytes,
@@ -164,6 +213,9 @@ export default {
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val; return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
if (DATETIME.includes(type)) { if (DATETIME.includes(type)) {
if (typeof val === 'string')
return val;
let datePrecision = ''; let datePrecision = '';
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S'; datePrecision += i === 0 ? '.S' : 'S';
@@ -185,15 +237,18 @@ export default {
return hexToBinary(hex); return hexToBinary(hex);
} }
if (ARRAY.includes(type)) {
if (Array.isArray(val))
return JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
return val;
}
return val; return val;
} }
}, },
props: { props: {
row: Object, row: Object,
fields: { fields: Object,
type: Array,
default: () => []
},
keyUsage: Array keyUsage: Array
}, },
data () { data () {
@@ -206,6 +261,8 @@ export default {
editingContent: null, editingContent: null,
editingType: null, editingType: null,
editingField: null, editingField: null,
editingLength: null,
editorMode: 'text',
contentInfo: { contentInfo: {
ext: '', ext: '',
mime: '', mime: '',
@@ -219,16 +276,19 @@ export default {
if ([...TEXT, ...LONG_TEXT].includes(this.editingType)) if ([...TEXT, ...LONG_TEXT].includes(this.editingType))
return { type: 'text', mask: false }; return { type: 'text', mask: false };
if (NUMBER.includes(this.editingType)) if ([...NUMBER, ...FLOAT].includes(this.editingType))
return { type: 'number', mask: false }; return { type: 'number', mask: false };
if (TIME.includes(this.editingType)) { if (TIME.includes(this.editingType)) {
let timeMask = '##:##:##'; let timeMask = '##:##:##';
const precision = this.getFieldPrecision(this.editingField); const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#'; timeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType))
timeMask += 'X##';
return { type: 'text', mask: timeMask }; return { type: 'text', mask: timeMask };
} }
@@ -237,19 +297,22 @@ export default {
if (DATETIME.includes(this.editingType)) { if (DATETIME.includes(this.editingType)) {
let datetimeMask = '####-##-## ##:##:##'; let datetimeMask = '####-##-## ##:##:##';
const precision = this.getFieldPrecision(this.editingField); const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#'; datetimeMask += i === 0 ? '.#' : '#';
if (HAS_TIMEZONE.includes(this.editingType))
datetimeMask += 'X##';
return { type: 'text', mask: datetimeMask }; return { type: 'text', mask: datetimeMask };
} }
if (BLOB.includes(this.editingType)) if (BLOB.includes(this.editingType))
return { type: 'file', mask: false }; return { type: 'file', mask: false };
if (BIT.includes(this.editingType)) if (BOOLEAN.includes(this.editingType))
return { type: 'text', mask: false }; return { type: 'boolean', mask: false };
return { type: 'text', mask: false }; return { type: 'text', mask: false };
}, },
@@ -260,80 +323,70 @@ export default {
return this.keyUsage.map(key => key.field); return this.keyUsage.map(key => key.field);
}, },
isEditable () { 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: { watch: {
fields () { fields () {
this.fields.forEach(field => { Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false; this.isInlineEditor[field.name] = false;
}); });
} }
}, },
methods: { methods: {
getFieldType (cKey) { isForeignKey (key) {
let type = 'unknown'; if (key.includes('.'))
const field = this.getFieldObj(cKey); key = key.split('.').pop();
if (field)
type = field.type;
return type.toLowerCase(); return this.foreignKeys.includes(key);
},
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];
}, },
isNull (value) { isNull (value) {
return value === null ? ' is-null' : ''; return value === null ? ' is-null' : '';
}, },
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
bufferToBase64 (val) { bufferToBase64 (val) {
return bufferToBase64(val); return bufferToBase64(val);
}, },
editON (event, content, field) { editON (event, content, field) {
if (!this.isEditable) return; if (!this.isEditable) return;
const type = this.getFieldType(field).toUpperCase(); ; window.addEventListener('keydown', this.onKey);
this.originalContent = content;
const type = this.fields[field].type.toUpperCase(); ;
this.originalContent = this.$options.filters.typeFormat(content, type, this.fields[field].length);
this.editingType = type; this.editingType = type;
this.editingField = field; 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.isTextareaEditor = true;
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type); this.editingContent = this.$options.filters.typeFormat(content, type);
return; return;
} }
if (BLOB.includes(type)) { if (BLOB.includes(type)) {
this.isBlobEditor = true; this.isBlobEditor = true;
this.editingContent = this.originalContent || ''; this.editingContent = content || '';
this.fileToUpload = null; this.fileToUpload = null;
this.willBeDeleted = false; this.willBeDeleted = false;
if (this.originalContent !== null) { if (content !== null) {
const buff = Buffer.from(this.editingContent); const buff = Buffer.from(this.editingContent);
if (buff.length) { if (buff.length) {
const hex = buff.toString('hex').substring(0, 8).toUpperCase(); const hex = buff.toString('hex').substring(0, 8).toUpperCase();
@@ -349,7 +402,7 @@ export default {
} }
// Inline editable fields // Inline editable fields
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.getFieldPrecision(field)); this.editingContent = this.originalContent;
this.$nextTick(() => { // Focus on input this.$nextTick(() => { // Focus on input
event.target.blur(); event.target.blur();
@@ -360,10 +413,18 @@ export default {
this.isInlineEditor = { ...this.isInlineEditor, ...obj }; this.isInlineEditor = { ...this.isInlineEditor, ...obj };
}, },
editOFF () { editOFF () {
if (!this.editingField) return;
this.isInlineEditor[this.editingField] = false; this.isInlineEditor[this.editingField] = false;
let content; let content;
if (!BLOB.includes(this.editingType)) { 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; content = this.editingContent;
} }
else { // Handle file upload else { // Handle file upload
@@ -378,13 +439,14 @@ export default {
} }
this.$emit('update-field', { this.$emit('update-field', {
field: this.getFieldObj(this.editingField).name, field: this.fields[this.editingField].name,
type: this.editingType, type: this.editingType,
content content
}); });
this.editingType = null; this.editingType = null;
this.editingField = null; this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}, },
hideEditorModal () { hideEditorModal () {
this.isTextareaEditor = false; this.isTextareaEditor = false;
@@ -416,14 +478,27 @@ export default {
}; };
this.willBeDeleted = true; this.willBeDeleted = true;
}, },
contextMenu (event, cell) {
this.$emit('update-field', event, cell);
},
selectRow (event, row) { selectRow (event, row) {
this.$emit('select-row', event, row); this.$emit('select-row', event, row);
}, },
getKeyUsage (keyName) { getKeyUsage (keyName) {
if (keyName.includes('.'))
return this.keyUsage.find(key => key.field === keyName.split('.').pop());
return this.keyUsage.find(key => key.field === keyName); 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 +528,7 @@ export default {
} }
.editor-field-info { .editor-field-info {
margin-top: 0.6rem; margin-top: 0.4rem;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
white-space: normal; white-space: normal;

View File

@@ -32,15 +32,45 @@
</div> </div>
</div> </div>
</div> </div>
<button <button
v-if="isTable"
class="btn btn-dark btn-sm" 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" /> <i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button> </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>
<div class="workspace-query-info"> <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"> <div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b> {{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
</div> </div>
@@ -53,13 +83,15 @@
</div> </div>
</div> </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 <WorkspaceQueryTable
v-if="results" v-if="results"
ref="queryTable" ref="queryTable"
:results="results" :results="results"
:tab-uid="tabUid" :tab-uid="tabUid"
:conn-uid="connection.uid" :conn-uid="connection.uid"
:is-selected="isSelected"
mode="table" mode="table"
@update-field="updateField" @update-field="updateField"
@delete-selected="deleteSelected" @delete-selected="deleteSelected"
@@ -74,21 +106,33 @@
@hide="hideAddModal" @hide="hideAddModal"
@reload="reloadTable" @reload="reloadTable"
/> />
<ModalFakerRows
v-if="isFakerModal"
:fields="fields"
:key-usage="keyUsage"
:tab-uid="tabUid"
@hide="hideFakerModal"
@reload="reloadTable"
/>
</div> </div>
</template> </template>
<script> <script>
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable'; import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import ModalNewTableRow from '@/components/ModalNewTableRow'; import ModalNewTableRow from '@/components/ModalNewTableRow';
import ModalFakerRows from '@/components/ModalFakerRows';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs'; import tableTabs from '@/mixins/tableTabs';
export default { export default {
name: 'WorkspaceTableTab', name: 'WorkspaceTableTab',
components: { components: {
BaseLoader,
WorkspaceQueryTable, WorkspaceQueryTable,
ModalNewTableRow ModalNewTableRow,
ModalFakerRows
}, },
mixins: [tableTabs], mixins: [tableTabs],
props: { props: {
@@ -102,6 +146,7 @@ export default {
results: [], results: [],
lastTable: null, lastTable: null,
isAddModal: false, isAddModal: false,
isFakerModal: false,
autorefreshTimer: 0, autorefreshTimer: 0,
refreshInterval: null, refreshInterval: null,
sortParams: {} sortParams: {}
@@ -109,13 +154,17 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}), }),
workspace () { workspace () {
return this.getWorkspace(this.connection.uid); return this.getWorkspace(this.connection.uid);
}, },
isSelected () { 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 () { fields () {
return this.results.length ? this.results[0].fields : []; return this.results.length ? this.results[0].fields : [];
@@ -205,6 +254,13 @@ export default {
hideAddModal () { hideAddModal () {
this.isAddModal = false; this.isAddModal = false;
}, },
showFakerModal () {
if (this.isQuering) return;
this.isFakerModal = true;
},
hideFakerModal () {
this.isFakerModal = false;
},
onKey (e) { onKey (e) {
if (this.isSelected) { if (this.isSelected) {
e.stopPropagation(); e.stopPropagation();
@@ -222,7 +278,17 @@ export default {
this.reloadTable(); this.reloadTable();
}, this.autorefreshTimer * 1000); }, this.autorefreshTimer * 1000);
} }
},
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, this.table);
} }
} }
}; };
</script> </script>
<style lang="scss" scoped>
.export-dropdown {
.menu {
min-width: 100%;
}
}
</style>

View File

@@ -69,19 +69,5 @@ module.exports = {
uploadFile: 'رفع ملف', uploadFile: 'رفع ملف',
addNewRow: 'إضافة صف جديد', addNewRow: 'إضافة صف جديد',
numberOfInserts: 'عدد الإدراجات' 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,33 @@ module.exports = {
parameters: 'Parameters', parameters: 'Parameters',
function: 'Function | Functions', function: 'Function | Functions',
deterministic: 'Deterministic', 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',
scratchpad: 'Scratchpad',
array: 'Array',
changelog: 'Changelog'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@@ -158,20 +184,194 @@ module.exports = {
thereAreNoParameters: 'There are no parameters', thereAreNoParameters: 'There are no parameters',
createNewParameter: 'Create new parameter', createNewParameter: 'Create new parameter',
createNewRoutine: 'Create new stored routine', 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',
markdownSupported: 'Markdown supported'
}, },
// Date and Time faker: {
short: { address: 'Address',
year: 'numeric', commerce: 'Commerce',
month: 'short', company: 'Company',
day: 'numeric' database: 'Database',
}, date: 'Date',
long: { finance: 'Finance',
year: 'numeric', git: 'Git',
month: 'short', hacker: 'Hacker',
day: 'numeric', internet: 'Internet',
weekday: 'short', lorem: 'Lorem',
hour: 'numeric', name: 'Name',
minute: 'numeric' 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', numberOfInserts: 'Numero de inserciones',
openNewTab: 'Abrir nueva pestaña', openNewTab: 'Abrir nueva pestaña',
affectedRows: 'Filas afectadas' 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'), 'en-US': require('./en-US'),
'it-IT': require('./it-IT'), 'it-IT': require('./it-IT'),
'ar-SA': require('./ar-SA'), 'ar-SA': require('./ar-SA'),
'es-ES': require('./es-ES') 'es-ES': require('./es-ES'),
'fr-FR': require('./fr-FR')
} }
}); });
export default i18n; export default i18n;

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