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

Compare commits

..

66 Commits

Author SHA1 Message Date
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
74 changed files with 5950 additions and 793 deletions

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,113 @@
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.0.18](https://github.com/Fabio286/antares/compare/v0.0.17...v0.0.18) (2021-02-25)
### Features
* **UI:** context menu for input and textarea tags ([b54fefb](https://github.com/Fabio286/antares/commit/b54fefbf255c91f3cdd0c52b917b239ea40dae16))
* **UI:** html, xml, json, svg and yaml editor modes in long text fields edit ([9a1bf32](https://github.com/Fabio286/antares/commit/9a1bf3212850d3783ac6382f8a980eb25c4f4226))
* **UI:** run procedures/functions from sidebar context menu ([219da0a](https://github.com/Fabio286/antares/commit/219da0aba46839295807a01056199d42584e0af9))
* **UI:** run routines/functions from settings tab ([7e81671](https://github.com/Fabio286/antares/commit/7e8167154ffd64e46d78a50cd13cf90cee61370c))
* **UI:** search filter in explore bar ([2f58007](https://github.com/Fabio286/antares/commit/2f58007af4f32207fe4be9fc15bb43916e8e0abc))
* **UI:** sticky schema names in explore bar ([110b0b4](https://github.com/Fabio286/antares/commit/110b0b414cef25395ae28493032c2048307e4783))
### Bug Fixes
* **MySQL:** issue obtaining routine/function parameters ([3aa2159](https://github.com/Fabio286/antares/commit/3aa2159a1ae3b6785646fc7ac1e866cb0277d087))
* issue managing function/routine parameters ([76d92cd](https://github.com/Fabio286/antares/commit/76d92cd106b0409e8752c43d7d587d09dd3e1e32))
* **UI:** data tab opened when non-table element is selected ([dbab06f](https://github.com/Fabio286/antares/commit/dbab06fcb80ccdae85215ceae9ba24af3e7ff263))
* **UI:** elements from previous selected schemas in query suggestions ([c8545a2](https://github.com/Fabio286/antares/commit/c8545a250bc696c339e418a69a2942d5bac10cbf))
* disabled sort for fields without a name property ([3b37b74](https://github.com/Fabio286/antares/commit/3b37b7432e969e6315ebd6ed74905ec3980a049c))
* prevents F5 shortcut to run in non-selected workspaces ([7c4ca99](https://github.com/Fabio286/antares/commit/7c4ca999ce64af8077d29e419f430d5beaaead93))
* support of bit fields in table filler ([94c4952](https://github.com/Fabio286/antares/commit/94c49523197284510361ebbb6984816a3bb1b243))
### [0.0.17](https://github.com/Fabio286/antares/compare/v0.0.16...v0.0.17) (2021-02-17)
### Features
* Added french language ([18a93ef](https://github.com/Fabio286/antares/commit/18a93ef1aae530e69c9062bbeb08a3beec206eda))
* fake table data generator ([a176174](https://github.com/Fabio286/antares/commit/a176174b8d7dc232920f4cd7c5e3f8e4c58d51a0))
* min and max option for random floats and numbers ([6c62052](https://github.com/Fabio286/antares/commit/6c62052b4764731c774fef90784342447b36deb7))
* support to fake data locales ([970de49](https://github.com/Fabio286/antares/commit/970de4962b3bffec271bfa6d4747e6fb9d408ba6))
### Bug Fixes
* **UI:** file uploader in table filler ([b5a8283](https://github.com/Fabio286/antares/commit/b5a828309f067636eae2120032f07e01233706e2))
* **UI:** no foreign key select editing query results ([5b21d17](https://github.com/Fabio286/antares/commit/5b21d17f3a8f2482c3aafe85f26c34cd3c0a1fcf))
* cut faker text based on field length ([288ff4c](https://github.com/Fabio286/antares/commit/288ff4c1a1c77f4b8b86b24649d805836366fdd3))
* wrong date or time detection in field default ([9d5ebef](https://github.com/Fabio286/antares/commit/9d5ebefdced999af595f3d8dc2fac2b18fa8258b))
* **UI:** better text on ssl file selectors ([9a19085](https://github.com/Fabio286/antares/commit/9a190854fe3c73ed4e7f89545ff259e15ee9f947))
* **UI:** wrong length for char fields on table header ([0f69d1d](https://github.com/Fabio286/antares/commit/0f69d1dbb7958e45059b6b738c845abea1ad3225))
### Improvements
* **core:** bulk inserts support ([b0576ac](https://github.com/Fabio286/antares/commit/b0576acdf65d41c1c8e0b0bca6cf6522dcb372be))
### [0.0.16](https://github.com/Fabio286/antares/compare/v0.0.15...v0.0.16) (2021-02-06)
### Features
* MySQL and MariaDB auto detection ([02c03e3](https://github.com/Fabio286/antares/commit/02c03e3d266052872ac8edeb159ec8182f41c6a5))
* **UI:** enanched file upload input ([a0d8552](https://github.com/Fabio286/antares/commit/a0d85520fb0669d5eec4475f5e255adb1e2a0159))
* support to ssl connections ([4e72bb1](https://github.com/Fabio286/antares/commit/4e72bb15874324214aa0f5b89057cdd565744468))
* **UI:** database version in app footer ([15417e8](https://github.com/Fabio286/antares/commit/15417e8a776c6aa9f90762d198d70b26163bb2df))
* **UI:** resize query editor area ([88ab7c5](https://github.com/Fabio286/antares/commit/88ab7c5a62654c6a6026b63464224226d33b1950))
* delete rows from tables without a primary key ([574d493](https://github.com/Fabio286/antares/commit/574d4939083577ffcb8e7c65f572c364eb8415fb))
* edit rows from tables without a primary key ([5940b0b](https://github.com/Fabio286/antares/commit/5940b0b84207093da141b5c617c41e0a18fcb1d7))
### Bug Fixes
* compatibility with electron-store 7 ([bacf458](https://github.com/Fabio286/antares/commit/bacf45893676cb0744907703f6534ffd472bd1dd))
* edit bit fields ([ede6fe8](https://github.com/Fabio286/antares/commit/ede6fe81cefc91cdce2bbb0cd7cd6f85bbca99b8))
### [0.0.15](https://github.com/Fabio286/antares/compare/v0.0.14...v0.0.15) (2021-01-23)
### Features
* functions and schedulers in query suggestions ([8ff6e70](https://github.com/Fabio286/antares/commit/8ff6e70145ed2a207ae8b23a2c688258382a5d74))
* loading animation in properties tabs ([1cf6485](https://github.com/Fabio286/antares/commit/1cf64858964f4894913db42f7c268013bb06e40b))
### Bug Fixes
* error retriving dato of some schedulers ([b9ed8dd](https://github.com/Fabio286/antares/commit/b9ed8dd610e3be1489e01cf53f7d632cb1bd6ac5))
* unable to call stored routines from query tabs ([4923128](https://github.com/Fabio286/antares/commit/4923128236131482ca948ae8052c294bd9269ed0))
### Improvements
* better fields type detection ([4bc9bbf](https://github.com/Fabio286/antares/commit/4bc9bbfb34ebdc51061f718cdf9cbca8507fa0f4))
* big performance improvement in database structure loading ([a11bac5](https://github.com/Fabio286/antares/commit/a11bac504cd4ee865ea6c614a15ee809dc38202e))
### [0.0.14](https://github.com/Fabio286/antares/compare/v0.0.13...v0.0.14) (2021-01-16)
### Features
* export data tables to json or csv file ([0cbea9d](https://github.com/Fabio286/antares/commit/0cbea9d1007304a5b9cf893d165b4b4104266651))
* functions creation ([49d7172](https://github.com/Fabio286/antares/commit/49d71722e26172232f7b54c6568e1e588ce0d049))
* functions delete ([59a50bc](https://github.com/Fabio286/antares/commit/59a50bc014facc9643f9153cff61dc9d5a8605a9))
* functions edit ([41d75b1](https://github.com/Fabio286/antares/commit/41d75b127cbcf1481fd259a14e6e7688638e18a4))
* scheduler edit ([ceab4ef](https://github.com/Fabio286/antares/commit/ceab4ef243881ba64517fb95320844a21fce4849))
* schedulers creation ([dbe7b9d](https://github.com/Fabio286/antares/commit/dbe7b9dd239248e806377ae6236b477456f175a3))
* schedulers delete ([1e7d4ca](https://github.com/Fabio286/antares/commit/1e7d4ca347f4b9337ff266ec78bb4bbc6dd20d4d))
* triggers and stored routines in sql suggestions ([e351c90](https://github.com/Fabio286/antares/commit/e351c903a8a8d7e908d6a7d54c0491438ac6f024))
### Bug Fixes
* error with empty functions/procedures ([f150508](https://github.com/Fabio286/antares/commit/f1505085477a760a768a7d245c9517a858c1379c))
* removed internal row _id from exported files ([c0a32c0](https://github.com/Fabio286/antares/commit/c0a32c040e653729ef80d580d6dd1796d1b2adcd))
### [0.0.13](https://github.com/Fabio286/antares/compare/v0.0.12...v0.0.13) (2021-01-06)

View File

@@ -32,6 +32,8 @@ An application created with minimalism and semplicity in mind, with features in
- Multiple database connections at same time.
- Database management (add/edit/delete).
- Full tables management, including indexes and foreign keys.
- Views, triggers, stored routines, functions and schedulers management (add/edit/delete).
- Fake table data filler.
- Run queries on multiple tabs.
- Query suggestions and auto complete.
- Native dark theme.
@@ -43,19 +45,28 @@ An application created with minimalism and semplicity in mind, with features in
This is a roadmap with major features will come in near future.
- Stored procedures, views, schedulers and triggers support.
- Users management (add/edit/delete).
- Database tools (variables, process list...).
- SSL and SSH tunnel support.
- Support for other databases.
- Database tools (variables, process list...).
- SSH tunnel support.
- Users management (add/edit/delete).
- UI/UX improvements.
- Query history.
- More context menu shortcuts.
- More keyboard shortcuts.
- Query logs console.
- Fake data filler.
- Import/export and migration.
- Themes.
- Light theme.
## Troubleshooting
### **Linux**
With KDE may need necessary installation of the additional `gnome-keyring` package.
Depending on your distribution, you will need to run the following command:
- Debian/Ubuntu: `sudo apt-get install gnome-keyring`
- Red Hat-based: `sudo yum install gnome-keyring`
- Arch Linux: `sudo pacman -S gnome-keyring`
## Currently supported
@@ -63,8 +74,8 @@ This is a roadmap with major features will come in near future.
- [x] MySQL/MariaDB
- [ ] PostgreSQL
- [ ] MSSQL
- [ ] SQLite
- [ ] MSSQL
- [ ] OracleDB
- [ ] More...
@@ -74,7 +85,7 @@ This is a roadmap with major features will come in near future.
- [x] Windows
- [x] Linux
- [x] MacOS (needs tests)
- [x] MacOS (i need feedbacks)
#### • ARM
@@ -84,6 +95,11 @@ This is a roadmap with major features will come in near future.
## Translations
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/Fabio286/antares/pull/20)
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/Fabio286/antares/pull/29)
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/Fabio286/antares/pull/32)
**Italian Translation** (46%) / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
**Arabic Translation** (45%) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
**Spanish Translation** (46%) / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
**French Translation** (100%) / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
## Reviews
<a target="_blank" href="https://www.softx64.com/windows/antares-sql-client.html" title="Antares SQL Client review"><img src="https://www.softx64.com/softx64-review.png" alt="Antares SQL Client review" /></a>

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.0.13",
"version": "0.0.18",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
@@ -39,6 +39,10 @@
"AppImage"
],
"category": "Development"
},
"appImage": {
"license": "./LICENSE",
"category": "Development"
}
},
"electronWebpack": {
@@ -47,47 +51,46 @@
}
},
"dependencies": {
"@mdi/font": "^5.8.55",
"@mdi/font": "^5.9.55",
"ace-builds": "^1.4.12",
"electron-log": "^4.3.0",
"electron-store": "^6.0.1",
"electron-store": "^7.0.0",
"electron-updater": "^4.3.5",
"faker": "^5.3.1",
"keytar": "^7.3.0",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"mssql": "^6.2.3",
"mysql": "^2.18.1",
"pg": "^8.5.1",
"source-map-support": "^0.5.16",
"spectre.css": "^0.5.9",
"vue-i18n": "^8.22.2",
"vue-i18n": "^8.22.4",
"vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0",
"vuex-persist": "^3.1.3"
"vuex": "^3.6.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.2",
"electron": "^11.0.2",
"electron": "^11.3.0",
"electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.14.0",
"eslint": "^7.20.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-vue": "^7.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-vue": "^7.6.0",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.0",
"standard-version": "^9.0.0",
"stylelint": "^13.8.0",
"sass-loader": "^10.1.1",
"standard-version": "^9.1.0",
"stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"stylelint-scss": "^3.19.0",
"vue": "^2.6.12",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.2"
"webpack": "^4.46.0"
}
}

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

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

View File

@@ -0,0 +1 @@
module.exports = [];

View File

@@ -1,12 +1,13 @@
export const TEXT = ['CHAR', 'VARCHAR'];
export const LONG_TEXT = ['TEXT', 'MEDIUMTEXT', 'LONGTEXT'];
export const NUMBER = ['INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL', 'BOOL'];
export const NUMBER = ['INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'BIGINT', 'DECIMAL', 'BOOL'];
export const FLOAT = ['FLOAT', 'DOUBLE'];
export const DATE = ['DATE'];
export const TIME = ['TIME'];
export const DATETIME = ['DATETIME', 'TIMESTAMP'];
export const BLOB = ['BLOB', 'MEDIUMBLOB', 'LONGBLOB'];
export const BLOB = ['BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB'];
export const BIT = ['BIT'];

View File

@@ -5,8 +5,12 @@ import * as path from 'path';
import crypto from 'crypto';
import { format as formatUrl } from 'url';
import keytar from 'keytar';
import Store from 'electron-store';
import ipcHandlers from './ipc-handlers';
Store.initRenderer();
const isDevelopment = process.env.NODE_ENV !== 'production';
const gotTheLock = app.requestSingleInstanceLock();

View File

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

View File

@@ -50,9 +50,9 @@ export default connections => {
}
});
ipcMain.handle('get-structure', async (event, uid) => {
ipcMain.handle('get-structure', async (event, params) => {
try {
const structure = await connections[uid].getStructure();
const structure = await connections[params.uid].getStructure(params.schemas);
return { status: 'success', response: structure };
}
@@ -94,6 +94,17 @@ 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('use-schema', async (event, { uid, schema }) => {
if (!schema) return;

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,6 +3,8 @@ import tables from './tables';
import views from './views';
import triggers from './triggers';
import routines from './routines';
import functions from './functions';
import schedulers from './schedulers';
import updates from './updates';
import application from './application';
import database from './database';
@@ -16,6 +18,8 @@ export default () => {
views(connections);
triggers(connections);
routines(connections);
functions(connections);
schedulers(connections);
database(connections);
users(connections);
updates();

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

@@ -1,6 +1,8 @@
import { ipcMain } from 'electron';
import faker from 'faker';
import moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import fs from 'fs';
export default (connections) => {
@@ -62,7 +64,7 @@ export default (connections) => {
let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
if (NUMBER.includes(params.type))
if ([...NUMBER, ...FLOAT].includes(params.type))
escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type))
escapedParam = `"${sqlEscaper(params.content)}"`;
@@ -75,15 +77,40 @@ export default (connections) => {
else
escapedParam = '""';
}
else if ([...BIT].includes(params.type)) {
escapedParam = `b'${sqlEscaper(params.content)}'`;
reload = true;
}
else
escapedParam = `"${sqlEscaper(params.content)}"`;
await connections[params.uid]
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${id}` })
.run();
if (params.primary) {
await connections[params.uid]
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${id}` })
.run();
}
else {
const { row } = params;
reload = true;
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)
.update({ [params.field]: `= ${escapedParam}` })
.from(params.table)
.where(row)
.limit(1)
.run();
}
return { status: 'success', response: { reload } };
}
@@ -93,24 +120,47 @@ export default (connections) => {
});
ipcMain.handle('delete-table-rows', async (event, params) => {
let idString;
if (params.primary) {
const idString = params.rows.map(row => typeof row[params.primary] === 'string'
? `"${row[params.primary]}"`
: row[params.primary]).join(',');
if (typeof params.rows[0] === 'string')
idString = params.rows.map(row => `"${row}"`).join(',');
else
idString = params.rows.join(',');
try {
const result = await connections[params.uid]
.schema(params.schema)
.delete(params.table)
.where({ [params.primary]: `IN (${idString})` })
.run();
try {
const result = await connections[params.uid]
.schema(params.schema)
.delete(params.table)
.where({ [params.primary]: `IN (${idString})` })
.run();
return { status: 'success', response: result };
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
}
catch (err) {
return { status: 'error', response: err.toString() };
else {
try {
for (const row of params.rows) {
for (const key in row) {
if (typeof row[key] === 'string')
row[key] = `'${row[key]}'`;
row[key] = `= ${row[key]}`;
}
await connections[params.uid]
.schema(params.schema)
.delete(params.table)
.where(row)
.limit(1)
.run();
}
return { status: 'success', response: [] };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
}
});
@@ -123,7 +173,7 @@ export default (connections) => {
if (params.row[key] === null)
escapedParam = 'NULL';
else if (NUMBER.includes(type))
else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type))
escapedParam = `"${sqlEscaper(params.row[key])}"`;
@@ -141,14 +191,92 @@ export default (connections) => {
insertObj[key] = escapedParam;
}
for (let i = 0; i < params.repeat; i++) {
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(insertObj)
.run();
const rows = new Array(+params.repeat).fill(insertObj);
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(rows)
.run();
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('insert-table-fake-rows', async (event, params) => {
try {
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))
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
else if (BLOB.includes(type)) {
if (params.row[key].value) {
const fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
}
else
escapedParam = '""';
}
else if (BIT.includes(type))
escapedParam = `b'${sqlEscaper(params.row[key].value)}'`;
else
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
insertObj[key] = escapedParam;
}
else { // Faker value
const parsedParams = {};
let fakeValue;
if (params.locale)
faker.locale = params.locale;
if (Object.keys(params.row[key].params).length) {
Object.keys(params.row[key].params).forEach(param => {
if (!isNaN(params.row[key].params[param]))
parsedParams[param] = +params.row[key].params[param];
});
fakeValue = faker[params.row[key].group][params.row[key].method](parsedParams);
}
else
fakeValue = faker[params.row[key].group][params.row[key].method]();
if (typeof fakeValue === 'string') {
if (params.row[key].length)
fakeValue = fakeValue.substr(0, params.row[key].length);
fakeValue = `"${sqlEscaper(fakeValue)}"`;
}
else if ([...DATE, ...DATETIME].includes(type))
fakeValue = `"${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}"`;
insertObj[key] = fakeValue;
}
}
rows.push(insertObj);
}
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(rows)
.run();
return { status: 'success' };
}
catch (err) {

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@
<slot name="body" />
</div>
</div>
<div class="modal-footer">
<div class="modal-footer pt-0">
<button
class="btn btn-primary mr-2"
@click.stop="confirmModal"

View File

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

View File

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

View File

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

View File

@@ -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 { mask } from 'vue-the-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
},
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

@@ -50,6 +50,7 @@ export default {
selectedWorkspace: 'workspaces/getSelected'
}),
isValidDefault () {
if (!this.foreignList.length) return true;
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString());
}
},

View File

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

View File

@@ -0,0 +1,102 @@
<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="`type-${parameter.type.toLowerCase()}`">
{{ parameter.type }} {{ parameter.length | wrapNumber }}
</span>
</div>
</div>
</div>
</form>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalAskParameters',
components: {
ConfirmModal
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
localRoutine: Object
},
data () {
return {
values: {}
};
},
created () {
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput[0].focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr) => {
const value = isNaN(this.values[curr]) ? `"${this.values[curr]}"` : this.values[curr];
acc.push(value);
return acc;
}, []);
this.$emit('confirm', valArr);
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style scoped>
.field-type {
font-size: 0.6rem;
}
</style>

View File

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

View File

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

View File

@@ -0,0 +1,391 @@
<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="`type-${field.type.toLowerCase()}`">
{{ 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 { mask } from 'vue-the-mask';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect';
export default {
name: 'ModalFakerRows',
components: {
FakerSelect
},
directives: {
mask
},
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'
}),
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,220 @@
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}:</label>
<div class="modal-body p-0">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="c-hand">{{ $t('word.general') }}</a>
</li>
<li
class="tab-item"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="c-hand">{{ $t('word.ssl') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}</label>
</div>
<div class="col-8 col-sm-12">
<select
v-model="connection.client"
class="form-select"
@change="setDefaults"
>
<option value="mysql">
MySQL
</option>
<option value="maria">
MariaDB
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="pg">
PostgreSQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12" />
<div class="col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsl') }}
</label>
</div>
<div class="col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="connection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}:</label>
</div>
<div class="col-8 col-sm-12">
<select
v-model="connection.client"
class="form-select"
@change="setDefaults"
>
<option value="mysql">
MySQL
</option>
<option value="maria">
MariaDB
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="pg">
PostgreSQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}:</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12" />
<div class="col-8 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }}
</label>
</div>
</div>
</fieldset>
</form>
<fieldset class="m-0" :disabled="isTesting || !connection.ssl">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="connection.key"
:message="$t('word.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.certificate') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="connection.cert"
:message="$t('word.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.caCertificate') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="connection.ca"
:message="$t('word.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div class="modal-footer text-light">
<button
@@ -154,12 +255,14 @@ import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen';
import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast';
import BaseUploadInput from '@/components/BaseUploadInput';
export default {
name: 'ModalNewConnection',
components: {
ModalAskCredentials,
BaseToast
BaseToast,
BaseUploadInput
},
data () {
return {
@@ -171,14 +274,21 @@ export default {
user: 'root',
password: '',
ask: false,
uid: uidGen('C')
uid: uidGen('C'),
ssl: false,
cert: '',
key: '',
ca: '',
ciphers: ''
},
toast: {
status: '',
message: ''
},
isTesting: false,
isAsking: false
isAsking: false,
selectedTab: 'general'
};
},
created () {
@@ -265,6 +375,21 @@ export default {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.connection.ssl = !this.connection.ssl;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.connection[name] = files[0].path;
},
pathClear (name) {
this.connection[name] = '';
}
}
};
@@ -272,6 +397,8 @@ export default {
<style scoped>
.modal-container {
position: absolute;
max-width: 450px;
top: 17.5vh;
}
</style>

View File

@@ -15,7 +15,7 @@
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}:</label>
<label class="form-label">{{ $t('word.name') }}</label>
</div>
<div class="col-9">
<input
@@ -30,7 +30,7 @@
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}:</label>
<label class="form-label">{{ $t('word.collation') }}</label>
</div>
<div class="col-9">
<select v-model="database.collation" class="form-select">

View File

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

View File

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

View File

@@ -50,6 +50,16 @@
:tabindex="key+1"
@change="filesChange($event,field.name)"
>
<input
v-else-if="inputProps(field).type === 'number'"
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
step="any"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else
ref="formInput"
@@ -107,7 +117,7 @@
<script>
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 { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
@@ -157,6 +167,8 @@ export default {
nInserts (val) {
if (!val || val < 1)
this.nInserts = 1;
else if (val > 1000)
this.nInserts = 1000;
}
},
created () {
@@ -170,7 +182,7 @@ export default {
if (field.default === 'NULL') fieldDefault = null;
else {
if (NUMBER.includes(field.type))
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = +field.default;
if ([...TEXT, ...LONG_TEXT].includes(field.type))
@@ -253,13 +265,14 @@ export default {
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numLength || field.datePrecision || field.charLength || 0;
else if (TEXT.includes(field.type)) return field.charLength;
return field.length;
},
inputProps (field) {
if ([...TEXT, ...LONG_TEXT].includes(field.type))
return { type: 'text', mask: false };
if (NUMBER.includes(field.type))
if ([...NUMBER, ...FLOAT].includes(field.type))
return { type: 'number', mask: false };
if (TIME.includes(field.type)) {

View File

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

View File

@@ -1,7 +1,7 @@
<template>
<div class="editor-wrapper">
<div
ref="editor"
:id="`editor-${id}`"
class="editor"
:style="{height: `${height}px`}"
/>
@@ -12,7 +12,7 @@
import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools';
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
export default {
@@ -20,6 +20,7 @@ export default {
props: {
value: String,
workspace: Object,
isSelected: Boolean,
schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
@@ -29,14 +30,17 @@ export default {
return {
editor: null,
fields: [],
baseCompleter: []
customCompleter: [],
id: null,
lastSchema: null
};
},
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
lineWrap: 'settings/getLineWrap',
baseCompleter: 'application/getBaseCompleter'
}),
tables () {
return this.workspace
@@ -47,13 +51,68 @@ export default {
}, []).map(table => {
return {
name: table.name,
comment: table.comment,
type: table.type,
fields: []
};
})
: [];
},
triggers () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.triggers);
return acc;
}, []).map(trigger => {
return {
name: trigger.name,
type: 'trigger'
};
})
: [];
},
procedures () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.procedures);
return acc;
}, []).map(procedure => {
return {
name: `${procedure.name}()`,
type: 'routine'
};
})
: [];
},
functions () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.functions);
return acc;
}, []).map(func => {
return {
name: `${func.name}()`,
type: 'function'
};
})
: [];
},
schedulers () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.schedulers);
return acc;
}, []).map(scheduler => {
return {
name: scheduler.name,
type: 'scheduler'
};
})
: [];
},
mode () {
switch (this.workspace.client) {
case 'mysql':
@@ -108,10 +167,24 @@ export default {
wrap: this.lineWrap
});
}
},
isSelected () {
if (this.isSelected)
this.lastSchema = this.schema;
},
lastSchema () {
if (this.editor) {
this.editor.completers = this.baseCompleter.map(el => Object.assign({}, el));
this.setCustomCompleter();
}
}
},
created () {
this.id = this._uid;
this.lastSchema = this.schema;
},
mounted () {
this.editor = ace.edit(this.$refs.editor, {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value,
@@ -127,21 +200,10 @@ export default {
enableLiveAutocompletion: this.autoComplete
});
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
this.tables.forEach(table => {
completions.push({
value: table.name,
meta: table.type,
caption: table.comment
});
});
callback(null, completions);
}
});
if (!this.baseCompleter.length)
this.setBaseCompleters(this.editor.completers.map(el => Object.assign({}, el)));
this.baseCompleter = this.editor.completers;
this.setCustomCompleter();
this.editor.commands.on('afterExec', e => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
@@ -164,13 +226,13 @@ export default {
}).catch(console.log);
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
});
@@ -189,6 +251,33 @@ export default {
setTimeout(() => {
this.editor.resize();
}, 20);
},
methods: {
...mapActions({
setBaseCompleters: 'application/setBaseCompleter'
}),
setCustomCompleter () {
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
this.customCompleter = this.editor.completers;
}
}
};
</script>

View File

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

View File

@@ -20,7 +20,7 @@
>
<a class="tab-link">
<i class="mdi mdi-18px mdi-tune mr-1" />
<span :title="schemaChild">{{ $t('word.properties').toUpperCase() }}: {{ schemaChild }}</span>
<span :title="schemaChild">{{ $t('word.settings').toUpperCase() }}: {{ schemaChild }}</span>
</a>
</li>
<li
@@ -89,6 +89,18 @@
:connection="connection"
:routine="workspace.breadcrumbs.procedure"
/>
<WorkspacePropsTabFunction
v-show="selectedTab === 'prop' && workspace.breadcrumbs.function"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:function="workspace.breadcrumbs.function"
/>
<WorkspacePropsTabScheduler
v-show="selectedTab === 'prop' && workspace.breadcrumbs.scheduler"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:scheduler="workspace.breadcrumbs.scheduler"
/>
<WorkspaceTableTab
v-show="selectedTab === 'data'"
:connection="connection"
@@ -97,7 +109,7 @@
<WorkspaceQueryTab
v-for="tab of queryTabs"
:key="tab.uid"
:tab-uid="tab.uid"
:tab="tab"
:is-selected="selectedTab === tab.uid"
:connection="connection"
/>
@@ -115,6 +127,8 @@ import WorkspacePropsTab from '@/components/WorkspacePropsTab';
import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
export default {
name: 'Workspace',
@@ -125,7 +139,9 @@ export default {
WorkspacePropsTab,
WorkspacePropsTabView,
WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine
WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabScheduler
},
props: {
connection: Object
@@ -148,13 +164,20 @@ export default {
},
selectedTab () {
if (
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
) ||
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.selected_tab === 'data'
)
)
return this.queryTabs[0].uid;
@@ -198,7 +221,7 @@ export default {
removeTab: 'workspaces/removeTab'
}),
addTab () {
this.newTab(this.connection.uid);
this.newTab({ uid: this.connection.uid });
if (!this.hasWheelEvent) {
this.$refs.tabWrap.addEventListener('wheel', e => {

View File

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

View File

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

View File

@@ -19,10 +19,10 @@
<div class="context-element" @click="showCreateRoutineModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div>
<div class="context-element disabled" @click="false">
<div class="context-element" @click="showCreateFunctionModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div>
<div class="context-element disabled" @click="false">
<div class="context-element" @click="showCreateSchedulerModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div>
</div>
@@ -108,6 +108,12 @@ export default {
showCreateRoutineModal () {
this.$emit('show-create-routine-modal');
},
showCreateFunctionModal () {
this.$emit('show-create-function-modal');
},
showCreateSchedulerModal () {
this.$emit('show-create-scheduler-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},

View File

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

View File

@@ -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,272 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> {{ $t('word.parameters') }} "{{ func }}"
</div>
</template>
<div :slot="'body'">
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')"
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
<div ref="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param._id}"
@click="selectParameter($event, param._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" />
</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: {
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, uid) {
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`,
type: 'INT',
context: 'IN',
length: 10
}];
if (this.parametersProxy.length === 1)
this.resetSelectedID();
setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20);
},
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID();
},
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (!this.parametersProxy.some(param => param.name === this.selectedParam))
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
}
}
};
</script>
<style lang="scss" scoped>
.tile {
border-radius: 2px;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;
.tile-action {
opacity: 0;
transition: opacity 0.2s;
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-param {
background: $bg-color-light;
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 300px;
overflow: auto;
}
.remove-field .mdi {
pointer-events: none;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -318,7 +318,7 @@ export default {
getWorkspace: 'workspaces/getWorkspace'
}),
localLength () {
return this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || 0;
return this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || this.localRow.numPrecision || 0;
},
fieldType () {
const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type =>
@@ -400,7 +400,7 @@ export default {
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default.replace(/(^')|('$)/g, '');
}
else if (!isNaN(this.localRow.default)) {
else if (!isNaN(this.localRow.default.replace(/[:.-\s]/g, ''))) {
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default;
}

View File

@@ -2,19 +2,23 @@
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<QueryEditor
v-if="isSelected"
v-show="isSelected"
ref="queryEditor"
:auto-focus="true"
:value.sync="query"
:workspace="workspace"
:schema="schema"
:is-selected="isSelected"
:height="editorHeight"
/>
<div ref="resizer" class="query-area-resizer" />
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:class="{'loading':isQuering}"
:disabled="!query"
title="F9"
title="F5"
@click="runQuery(query)"
>
<span>{{ $t('word.run') }}</span>
@@ -44,7 +48,7 @@
v-show="!isQuering"
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:tab-uid="tab.uid"
:conn-uid="connection.uid"
mode="query"
@update-field="updateField"
@@ -70,7 +74,7 @@ export default {
mixins: [tableTabs],
props: {
connection: Object,
tabUid: String,
tab: Object,
isSelected: Boolean
},
data () {
@@ -80,20 +84,40 @@ export default {
isQuering: false,
results: [],
resultsCount: 0,
affectedCount: 0
affectedCount: 0,
editorHeight: 200
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isWorkspaceSelected () {
return this.workspace.uid === this.selectedWorkspace;
}
},
created () {
this.query = this.tab.content;
window.addEventListener('keydown', this.onKey);
},
mounted () {
const resizer = this.$refs.resizer;
resizer.addEventListener('mousedown', e => {
e.preventDefault();
window.addEventListener('mousemove', this.resize);
window.addEventListener('mouseup', this.stopResize);
});
if (this.tab.autorun)
this.runQuery(this.query);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
@@ -139,10 +163,25 @@ export default {
this.resultsCount = 0;
this.affectedCount = 0;
},
resize (e) {
const el = this.$refs.queryEditor.$el;
let editorHeight = e.pageY - el.getBoundingClientRect().top;
if (editorHeight > 400) editorHeight = 400;
if (editorHeight < 50) editorHeight = 50;
this.editorHeight = editorHeight;
},
stopResize () {
window.removeEventListener('mousemove', this.resize);
if (this.$refs.queryTable && this.results.length)
this.$refs.queryTable.resizeResults();
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.resize();
},
onKey (e) {
if (this.isSelected) {
if (this.isSelected && this.isWorkspaceSelected) {
e.stopPropagation();
if (e.key === 'F9')
if (e.key === 'F5')
this.runQuery(this.query);
}
}
@@ -155,11 +194,23 @@ export default {
align-content: baseline;
.workspace-query-runner {
position: relative;
.query-area-resizer {
position: absolute;
height: 5px;
bottom: 40px;
width: 100%;
cursor: ns-resize;
z-index: 99;
}
.workspace-query-runner-footer {
display: flex;
justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem;
align-items: center;
height: 42px;
.workspace-query-buttons {
display: flex;

View File

@@ -41,7 +41,7 @@
/>
<span>{{ field.alias || field.name }}</span>
<i
v-if="currentSort === field.name || currentSort === `${field.table}.${field.name}`"
v-if="idSortable && currentSort === field.name || currentSort === `${field.table}.${field.name}`"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
@@ -69,7 +69,7 @@
class="tr"
:class="{'selected': selectedRows.includes(row._id)}"
@select-row="selectRow($event, row._id)"
@update-field="updateField($event, getPrimaryValue(row))"
@update-field="updateField($event, row)"
@contextmenu="contextMenu"
/>
</template>
@@ -80,7 +80,8 @@
<script>
import { uidGen } from 'common/libs/uidGen';
import { LONG_TEXT, BLOB } from 'common/fieldTypes';
import arrayToFile from '../libs/arrayToFile';
import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext';
@@ -123,6 +124,9 @@ export default {
primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
},
idSortable () {
return this.fields.every(field => field.name);
},
isHardSort () {
return this.mode === 'table' && this.localResults.length === 1000;
},
@@ -195,7 +199,8 @@ export default {
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numLength || field.datePrecision || field.charLength || 0;
else if (TEXT.includes(field.type)) return field.charLength;
return field.length;
},
keyName (key) {
switch (key) {
@@ -252,37 +257,32 @@ export default {
refreshScroller () {
this.resizeResults();
},
updateField (payload, id) {
if (!this.primaryField)
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') });
else {
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id,
...payload
};
this.$emit('update-field', params);
}
updateField (payload, row) {
delete row._id;
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(row),
row,
...payload
};
this.$emit('update-field', params);
},
deleteSelected () {
if (!this.primaryField)
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') });
else {
const rowIDs = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row =>
row[this.primaryField.name] ||
row[`${this.primaryField.table}.${this.primaryField.name}`] ||
row[`${this.primaryField.tableAlias}.${this.primaryField.name}`]
);
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
rows: rowIDs
};
this.$emit('delete-selected', params);
}
const rows = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => {
delete row._id;
return row;
});
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
rows
};
this.$emit('delete-selected', params);
},
applyUpdate (params) {
const { primary, id, field, table, content } = params;
@@ -324,6 +324,8 @@ export default {
this.selectedRows = [row];
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.selectedCell = cell;
if (!this.selectedRows.includes(cell.id))
this.selectedRows = [cell.id];
@@ -331,6 +333,8 @@ export default {
this.isContext = true;
},
sort (field) {
if (!this.idSortable) return;
if (this.mode === 'query')
field = `${this.getTable(this.resultsetIndex)}.${field}`;
@@ -354,6 +358,20 @@ export default {
},
selectResultset (index) {
this.resultsetIndex = index;
},
downloadTable (format, filename) {
if (!this.sortedResults) return;
const rows = [...this.sortedResults].map(row => {
delete row._id;
return row;
});
arrayToFile({
type: format,
content: rows,
filename
});
}
}
};

View File

@@ -16,7 +16,7 @@
@dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(getFieldType(cKey), getFieldPrecision(cKey)) | cutText }}</span>
<ForeignKeySelect
v-else-if="foreignKeys.includes(cKey)"
v-else-if="isForeignKey(cKey)"
class="editable-field"
:value.sync="editingContent"
:key-usage="getKeyUsage(cKey)"
@@ -61,14 +61,52 @@
<div :slot="'body'">
<div class="mb-2">
<div>
<textarea
v-model="editingContent"
class="form-input textarea-editor"
<TextEditor
:value.sync="editingContent"
editor-class="textarea-editor"
:mode="editorMode"
/>
</div>
<div class="editor-field-info">
<div><b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}</div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
<div class="editor-field-info p-vcentered">
<div class="d-flex p-vcentered">
<label for="editorMode" class="form-label mr-2">
<b>{{ $t('word.content') }}</b>:
</label>
<select
id="editorMode"
v-model="editorMode"
class="form-select select-sm"
>
<option value="text">
TEXT
</option>
<option value="html">
HTML
</option>
<option value="xml">
XML
</option>
<option value="json">
JSON
</option>
<option value="svg">
SVG
</option>
<option value="yaml">
YAML
</option>
</select>
</div>
<div class="d-flex">
<div class="p-vcentered">
<div class="mr-4">
<b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}
</div>
<div>
<b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -135,15 +173,17 @@ import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
name: 'WorkspaceQueryTableRow',
components: {
ConfirmModal,
TextEditor,
ForeignKeySelect
},
directives: {
@@ -206,6 +246,7 @@ export default {
editingContent: null,
editingType: null,
editingField: null,
editorMode: 'text',
contentInfo: {
ext: '',
mime: '',
@@ -219,7 +260,7 @@ export default {
if ([...TEXT, ...LONG_TEXT].includes(this.editingType))
return { type: 'text', mask: false };
if (NUMBER.includes(this.editingType))
if ([...NUMBER, ...FLOAT].includes(this.editingType))
return { type: 'number', mask: false };
if (TIME.includes(this.editingType)) {
@@ -260,7 +301,19 @@ export default {
return this.keyUsage.map(key => key.field);
},
isEditable () {
return this.fields ? !!(this.fields[0].schema && this.fields[0].table) : false;
if (this.fields) {
const nElements = this.fields.reduce((acc, curr) => {
acc.add(curr.table);
acc.add(curr.schema);
return acc;
}, new Set([]));
if (nElements.size > 2) return false;
return !!(this.fields[0].schema && this.fields[0].table);
}
return false;
}
},
watch: {
@@ -271,6 +324,12 @@ export default {
}
},
methods: {
isForeignKey (key) {
if (key.includes('.'))
key = key.split('.').pop();
return this.foreignKeys.includes(key);
},
getFieldType (cKey) {
let type = 'unknown';
const field = this.getFieldObj(cKey);
@@ -423,6 +482,8 @@ export default {
this.$emit('select-row', event, row);
},
getKeyUsage (keyName) {
if (keyName.includes('.'))
return this.keyUsage.find(key => key.field === keyName.split('.').pop());
return this.keyUsage.find(key => key.field === keyName);
}
}
@@ -453,7 +514,7 @@ export default {
}
.editor-field-info {
margin-top: 0.6rem;
margin-top: 0.4rem;
display: flex;
justify-content: space-between;
white-space: normal;

View File

@@ -32,13 +32,36 @@
</div>
</div>
</div>
<button
v-if="isTable"
class="btn btn-dark btn-sm"
@click="showAddModal"
:disabled="isQuering"
@click="showFakerModal"
>
<span>{{ $t('word.add') }}</span>
<span>{{ $t('message.tableFiller') }}</span>
<i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button>
<div class="dropdown export-dropdown pr-2">
<button
:disabled="isQuering"
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
tabindex="0"
>
<span>{{ $t('word.export') }}</span>
<i class="mdi mdi-24px mdi-file-export ml-1" />
<i class="mdi mdi-24px mdi-menu-down" />
</button>
<ul class="menu text-left">
<li class="menu-item">
<a class="c-hand" @click="downloadTable('json')">JSON</a>
</li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li>
</ul>
</div>
</div>
<div class="workspace-query-info">
<div v-if="results.length && results[0].rows">
@@ -74,6 +97,14 @@
@hide="hideAddModal"
@reload="reloadTable"
/>
<ModalFakerRows
v-if="isFakerModal"
:fields="fields"
:key-usage="keyUsage"
:tab-uid="tabUid"
@hide="hideFakerModal"
@reload="reloadTable"
/>
</div>
</template>
@@ -81,6 +112,7 @@
import Tables from '@/ipc-api/Tables';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import ModalNewTableRow from '@/components/ModalNewTableRow';
import ModalFakerRows from '@/components/ModalFakerRows';
import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs';
@@ -88,7 +120,8 @@ export default {
name: 'WorkspaceTableTab',
components: {
WorkspaceQueryTable,
ModalNewTableRow
ModalNewTableRow,
ModalFakerRows
},
mixins: [tableTabs],
props: {
@@ -102,6 +135,7 @@ export default {
results: [],
lastTable: null,
isAddModal: false,
isFakerModal: false,
autorefreshTimer: 0,
refreshInterval: null,
sortParams: {}
@@ -109,13 +143,17 @@ export default {
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'data';
return this.workspace.selected_tab === 'data' && this.workspace.uid === this.selectedWorkspace;
},
isTable () {
return !!this.workspace.breadcrumbs.table;
},
fields () {
return this.results.length ? this.results[0].fields : [];
@@ -205,6 +243,13 @@ export default {
hideAddModal () {
this.isAddModal = false;
},
showFakerModal () {
if (this.isQuering) return;
this.isFakerModal = true;
},
hideFakerModal () {
this.isFakerModal = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
@@ -222,7 +267,21 @@ export default {
this.reloadTable();
}, this.autorefreshTimer * 1000);
}
},
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, this.table);
}
}
};
</script>
<style lang="scss" scoped>
.export-dropdown {
.menu {
min-width: 100%;
.menu-item a:hover {
background: $bg-color-gray;
}
}
}
</style>

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -18,8 +18,8 @@ export default class {
return ipcRenderer.invoke('delete-database', params);
}
static getStructure (uid) {
return ipcRenderer.invoke('get-structure', uid);
static getStructure (params) {
return ipcRenderer.invoke('get-structure', params);
}
static getCollations (uid) {
@@ -34,6 +34,10 @@ export default class {
return ipcRenderer.invoke('get-engines', uid);
}
static getVersion (uid) {
return ipcRenderer.invoke('get-version', uid);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -210,6 +210,10 @@ body {
border-color: $primary-color;
}
.form-input[type="file"] {
overflow: hidden;
}
.input-group .input-group-addon {
border-color: #3f3f3f;
z-index: 1;

View File

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

View File

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

View File

@@ -40,6 +40,12 @@ export default {
.filter(workspace => workspace.connected)
.map(workspace => workspace.uid);
},
getLoadedSchemas: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).loaded_schemas;
},
getSearchTerm: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).search_term;
},
isUnsavedDiscardModal: state => {
return state.is_unsaved_discard_modal;
}
@@ -48,7 +54,9 @@ export default {
SELECT_WORKSPACE (state, uid) {
state.selected_workspace = uid;
},
ADD_CONNECTED (state, { uid, client, dataTypes, indexTypes, structure }) {
ADD_CONNECTED (state, payload) {
const { uid, client, dataTypes, indexTypes, structure, version } = payload;
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
@@ -56,7 +64,8 @@ export default {
dataTypes,
indexTypes,
structure,
connected: true
connected: true,
version
}
: workspace);
},
@@ -65,6 +74,8 @@ export default {
? {
...workspace,
structure: {},
breadcrumbs: {},
loaded_schemas: new Set(),
connected: false
}
: workspace);
@@ -77,6 +88,19 @@ export default {
}
: workspace);
},
REFRESH_SCHEMA (state, { uid, schema, schemaElements }) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
const schemaIndex = workspace.structure.findIndex(s => s.name === schema);
if (schemaIndex !== -1)
workspace.structure[schemaIndex] = schemaElements;
else
workspace.structure.push(schemaElements);
}
return workspace;
});
},
REFRESH_COLLATIONS (state, { uid, collations }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
@@ -120,7 +144,15 @@ export default {
}
: workspace);
},
NEW_TAB (state, { uid, tab }) {
SET_SEARCH_TERM (state, { uid, term }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
search_term: term
}
: workspace);
},
NEW_TAB (state, { uid, tab, content, autorun }) {
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
const newTab = {
uid: tab,
@@ -128,7 +160,9 @@ export default {
selected: false,
type: 'query',
fields: [],
keyUsage: []
keyUsage: [],
content: content || '',
autorun: !!autorun
};
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
@@ -198,6 +232,13 @@ export default {
},
SET_PENDING_BREADCRUMBS (state, payload) {
state.pending_breadcrumbs = payload;
},
ADD_LOADED_SCHEMA (state, payload) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === payload.uid)
workspace.loaded_schemas.add(payload.schema);
return workspace;
});
}
},
actions: {
@@ -220,12 +261,32 @@ export default {
indexTypes = require('common/index-types/mysql');
break;
}
const { status, response: version } = await Database.getVersion(connection.uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: version }, { root: true });
const isMySQL = version.name.includes('MySQL');
if (isMySQL && connection.client !== 'mysql') {
const connProxy = Object.assign({}, connection);
connProxy.client = 'mysql';
dispatch('connections/editConnection', connProxy, { root: true });
}
else if (!isMySQL && connection.client === 'mysql') {
const connProxy = Object.assign({}, connection);
connProxy.client = 'maria';
dispatch('connections/editConnection', connProxy, { root: true });
}
commit('ADD_CONNECTED', {
uid: connection.uid,
client: connection.client,
dataTypes,
indexTypes,
structure: response
structure: response,
version
});
dispatch('refreshCollations', connection.uid);
dispatch('refreshVariables', connection.uid);
@@ -237,9 +298,10 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshStructure ({ dispatch, commit }, uid) {
async refreshStructure ({ dispatch, commit, getters }, uid) {
try {
const { status, response } = await Database.getStructure(uid);
const { status, response } = await Database.getStructure({ uid, schemas: getters.getLoadedSchemas(uid) });
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -249,6 +311,18 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshSchema ({ dispatch, commit }, { uid, schema }) {
try {
const { status, response } = await Database.getStructure({ uid, schemas: new Set([schema]) });
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('REFRESH_SCHEMA', { uid, schema, schemaElements: response.find(_schema => _schema.name === schema) });
}
catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshCollations ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getCollations(uid);
@@ -307,29 +381,20 @@ export default {
uid,
connected: false,
selected_tab: 0,
tabs: [{
uid: 'data',
type: 'table',
fields: [],
keyUsage: []
},
{
uid: 'prop',
type: 'table',
fields: [],
keyUsage: []
}],
search_term: '',
tabs: [],
structure: {},
variables: [],
collations: [],
users: [],
breadcrumbs: {}
breadcrumbs: {},
loaded_schemas: new Set()
};
commit('ADD_WORKSPACE', workspace);
if (getters.getWorkspace(uid).tabs.length < 3)
dispatch('newTab', uid);
dispatch('newTab', { uid });
dispatch('setUnsavedChanges', false);
},
@@ -360,11 +425,17 @@ export default {
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...breadcrumbsObj, ...payload } });
lastBreadcrumbs = { ...breadcrumbsObj, ...payload };
if (payload.schema)
commit('ADD_LOADED_SCHEMA', { uid: getters.getSelected, schema: payload.schema });
},
newTab ({ commit }, uid) {
setSearchTerm ({ commit, getters }, term) {
commit('SET_SEARCH_TERM', { uid: getters.getSelected, term });
},
newTab ({ commit }, { uid, content, autorun }) {
const tab = uidGen('T');
commit('NEW_TAB', { uid, tab });
commit('NEW_TAB', { uid, tab, content, autorun });
commit('SELECT_TAB', { uid, tab });
},
removeTab ({ commit }, payload) {