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

Compare commits

...

60 Commits

Author SHA1 Message Date
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
27fd9ec203 chore(release): 0.0.13 2021-01-06 12:00:30 +01:00
0ec2710872 fix: wrong new stored routine modal icon 2021-01-06 12:00:09 +01:00
3bcd02fc4e feat: stored routines creation 2021-01-06 11:57:49 +01:00
aa33850286 feat: stored routines delete 2021-01-06 11:07:55 +01:00
82fdc0bcd7 feat: stored routines edit 2021-01-05 17:25:18 +01:00
d695c9f8d2 feat: triggers creation 2021-01-02 15:27:02 +01:00
b32132ad84 feat: triggers delete 2021-01-02 14:46:27 +01:00
3126625461 feat: triggers edit 2020-12-31 19:55:02 +01:00
ab307f82b1 feat: select definer in view creation/edit 2020-12-29 10:35:46 +01:00
0df2b836b1 fix: wrong or duplicate fields in some queries 2020-12-28 17:46:23 +01:00
6611aad840 perf: improved performance getting database structure 2020-12-28 13:05:30 +01:00
8c4aaec167 feat: views creation 2020-12-27 16:16:48 +01:00
b7053bdf80 fix: unable to rename views 2020-12-27 13:14:41 +01:00
b6b7be098a fix: breadcrumb not change after table rename 2020-12-27 13:08:13 +01:00
56f2a27f00 feat: views edit 2020-12-26 15:37:34 +01:00
dcf469ebed feat: views deletion 2020-12-26 14:47:15 +01:00
d94b49febf feat: option to toggle line wrap mode 2020-12-24 15:33:51 +01:00
3b4f1475df chore(release): 0.0.12 2020-12-24 10:58:03 +01:00
155154b43d feat: option to toggle editor auto completion 2020-12-24 10:40:22 +01:00
a95b8d188c feat: option to change editor theme 2020-12-23 18:07:50 +01:00
cb1fce6f99 feat: query editor auto-completer for tables and columns 2020-12-22 22:31:31 +01:00
0014f48079 refactor: migrated to ace from monaco-editor 2020-12-21 11:06:41 +01:00
fc35f271d7 feat: better security connections credentials storage 2020-12-18 18:44:32 +01:00
65ad0e954d ci: update travis configuration 2020-12-18 17:38:06 +01:00
8cafade8b1 chore(release): 0.0.11 2020-12-15 17:24:04 +01:00
d13b708377 chore: update REDME.md 2020-12-15 17:23:24 +01:00
206597e5b8 feat: foreign keys management 2020-12-15 17:08:36 +01:00
c5458159d1 fix: unable to switch tabs when no table selected 2020-12-11 18:22:07 +01:00
1476e899d1 feat: auto focus on first input in modals 2020-12-11 18:09:17 +01:00
797ab70e7c chore: update links 2020-12-11 16:05:32 +01:00
f81312aeb0 feat: query tabs auto focus 2020-12-11 15:55:18 +01:00
3ed5ea023e fix: some properties do not reset after fields changes 2020-12-11 12:57:24 +01:00
9291a7a7b4 fix: file field editor not show 2020-12-10 15:15:32 +01:00
5cfdc9b92d fix: wrong field type detection 2020-12-09 18:22:46 +01:00
15b08d7ea8 fix: data tab sort not maintained at refresh 2020-12-08 18:41:08 +01:00
acebe435ff fix: improved changes dedection in props tab 2020-12-07 19:11:29 +01:00
5712b80022 feat: improved data table sorts 2020-12-07 17:51:48 +01:00
d38583262e fix: deletion of rows with non-numeric ID 2020-12-07 15:07:59 +01:00
e0e2131981 chore: update README.md 2020-12-04 11:43:27 +01:00
91 changed files with 9201 additions and 6673 deletions

7
.gitattributes vendored
View File

@@ -1 +1,6 @@
* text eol=lf * text eol=lf
*.jpg binary
*.png binary
*.gif binary
*.ico binary
*.icns binary

View File

@@ -31,11 +31,6 @@ jobs:
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
fetch-depth: 2 fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1

View File

@@ -1,9 +1,6 @@
language: node_js language: node_js
node_js: 12 node_js: 12
before_install:
- npm install
cache: cache:
directories: directories:
- node_modules - node_modules
@@ -20,6 +17,9 @@ env:
jobs: jobs:
include: include:
- stage: Test - stage: Test
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script: script:
- npm test - npm test
@@ -27,6 +27,9 @@ jobs:
if: tag IS present if: tag IS present
os: linux os: linux
services: docker services: docker
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script: script:
- docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always" - docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always"
before_cache: before_cache:
@@ -35,6 +38,8 @@ jobs:
- stage: Deploy Mac - stage: Deploy Mac
if: tag IS present if: tag IS present
os: osx os: osx
before_install:
- npm install
osx_image: xcode10.2 osx_image: xcode10.2
script: script:
- npm run build -- -p always - npm run build -- -p always

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,102 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.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)
### Features
* option to toggle line wrap mode ([d94b49f](https://github.com/Fabio286/antares/commit/d94b49febf54b0200127859f2a8ed7ef591e56ab))
* select definer in view creation/edit ([ab307f8](https://github.com/Fabio286/antares/commit/ab307f82b1d78c5f9571233090b6678a964bd674))
* stored routines creation ([3bcd02f](https://github.com/Fabio286/antares/commit/3bcd02fc4ea9a4b780305212f906d6d78c7a8dae))
* stored routines delete ([aa33850](https://github.com/Fabio286/antares/commit/aa3385028685417860b3ce985cc7a74f9da377ad))
* stored routines edit ([82fdc0b](https://github.com/Fabio286/antares/commit/82fdc0bcd7514b321c1c9852a773adacf81baf87))
* triggers creation ([d695c9f](https://github.com/Fabio286/antares/commit/d695c9f8d2418a6a4523a7a242fa1a8cba80e035))
* triggers delete ([b32132a](https://github.com/Fabio286/antares/commit/b32132ad84d5798555b80eec3c624b681c37c339))
* triggers edit ([3126625](https://github.com/Fabio286/antares/commit/3126625461f4b6d68d641b6b0eda8fcd390bb636))
* views creation ([8c4aaec](https://github.com/Fabio286/antares/commit/8c4aaec167f58333a343b52927205b68137ad408))
* views deletion ([dcf469e](https://github.com/Fabio286/antares/commit/dcf469ebed6252b4a496800206e0c34cd83b1f5e))
* views edit ([56f2a27](https://github.com/Fabio286/antares/commit/56f2a27f0059cc10316204210db078a97408973c))
### Bug Fixes
* breadcrumb not change after table rename ([b6b7be0](https://github.com/Fabio286/antares/commit/b6b7be098ad5ab4d55bfe05a7f862f045c1f54da))
* unable to rename views ([b7053bd](https://github.com/Fabio286/antares/commit/b7053bdf8036d027e1685d6b5080d6b927a80e08))
* wrong new stored routine modal icon ([0ec2710](https://github.com/Fabio286/antares/commit/0ec2710872c692b3feac076a3250d3b760af4009))
* wrong or duplicate fields in some queries ([0df2b83](https://github.com/Fabio286/antares/commit/0df2b836b15436a2397d6a2202bd049b5cd53de4))
### [0.0.12](https://github.com/Fabio286/antares/compare/v0.0.11...v0.0.12) (2020-12-24)
### Features
* better security connections credentials storage ([fc35f27](https://github.com/Fabio286/antares/commit/fc35f271d7fe384cd786ce33547c0ef17135ddd8))
* option to change editor theme ([a95b8d1](https://github.com/Fabio286/antares/commit/a95b8d188cfcc8f563ad73b4f0b676d068775d36))
* option to toggle editor auto completion ([155154b](https://github.com/Fabio286/antares/commit/155154b43d0cd02ae875ded3ce865a37a999da5c))
* query editor auto-completer for tables and columns ([cb1fce6](https://github.com/Fabio286/antares/commit/cb1fce6f998ea7332886820910e245ab19416a9d))
### [0.0.11](https://github.com/Fabio286/antares/compare/v0.0.10...v0.0.11) (2020-12-15)
### Features
* auto focus on first input in modals ([1476e89](https://github.com/Fabio286/antares/commit/1476e899d164562f12342ced8c76903b9bdcfa55))
* foreign keys management ([206597e](https://github.com/Fabio286/antares/commit/206597e5b891e13e6f7635075bd11599355ab778))
* improved data table sorts ([5712b80](https://github.com/Fabio286/antares/commit/5712b8002203b32027f0e820f98a61e2ec965e79))
* query tabs auto focus ([f81312a](https://github.com/Fabio286/antares/commit/f81312aeb02ef55affd2ae9e81a9b4cb4c2e6da2))
### Bug Fixes
* data tab sort not maintained at refresh ([15b08d7](https://github.com/Fabio286/antares/commit/15b08d7ea858cb28111c7b548af9a13b1bf0da91))
* deletion of rows with non-numeric ID ([d385832](https://github.com/Fabio286/antares/commit/d38583262e672a2b47c5ad0aca0f13c129830a7b))
* file field editor not show ([9291a7a](https://github.com/Fabio286/antares/commit/9291a7a7b41e7aeb9b65c7f32e496f523e482272))
* improved changes dedection in props tab ([acebe43](https://github.com/Fabio286/antares/commit/acebe435ff6fa1581692fbf7ee1bc23b334e3947))
* some properties do not reset after fields changes ([3ed5ea0](https://github.com/Fabio286/antares/commit/3ed5ea023e1852d724b2b59ab156f8722876f85b))
* unable to switch tabs when no table selected ([c545815](https://github.com/Fabio286/antares/commit/c5458159d1e30cecf6615086c074d98b4b599637))
* wrong field type detection ([5cfdc9b](https://github.com/Fabio286/antares/commit/5cfdc9b92d4b778a7863b02fd64e6445236c89bc))
### [0.0.10](https://github.com/Fabio286/antares/compare/v0.0.9...v0.0.10) (2020-12-04) ### [0.0.10](https://github.com/Fabio286/antares/compare/v0.0.9...v0.0.10) (2020-12-04)

View File

@@ -9,7 +9,9 @@
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
My target is to support as many databases as possible, and all major operating systems, including the ARM versions. My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is an alpha, it lacks many features, and isn't ready as a main SQL client**. However i'm actively working on it (yes, i'm a lone dev), hoping to provide all essential features as soon as possible. **At the moment this application is an alpha, it lacks many features** and supports only MySQL.
Most of its current features might be enough for basic MySQL use, so give it a chance and send me your feedback, I would really appreciate it.
I'm actively working on it (yes, i'm a lone dev), hoping to provide cool features and fixes as soon as possible.
🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases). 🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases).
👁 To stay tuned for new releases watch this repo on **Release only** channel. 👁 To stay tuned for new releases watch this repo on **Release only** channel.
@@ -29,34 +31,31 @@ An application created with minimalism and semplicity in mind, with features in
- Multiple database connections at same time. - Multiple database connections at same time.
- Database management (add/edit/delete). - Database management (add/edit/delete).
- Tables fields management (add/edit/delete). - Full tables management, including indexes and foreign keys.
- Tables content management (add/edit/delete). - Views, triggers, stored routines, functions and schedulers management (add/edit/delete).
- Run queries on multiple tabs. - Run queries on multiple tabs.
- Query suggestions. - Query suggestions and auto complete.
- Native dark theme. - Native dark theme.
- Multi language. - Multi language.
- Secure password storage.
- Auto updates. - Auto updates.
## Coming soon ## Coming soon
This is a roadmap with major features will come in near future. This is a roadmap with major features will come in near future.
- Tables management (add/edit/delete).
- Users management (add/edit/delete).
- Stored procedures, views, schedulers and triggers support.
- More secure password storage.
- Database tools (variables, process list...).
- Support for other databases. - Support for other databases.
- Improvements of query editor area. - Database tools (variables, process list...).
- Improvements of query suggestions. - SSL and SSH tunnel support.
- Users management (add/edit/delete).
- UI/UX improvements.
- Query history. - Query history.
- More context menu shortcuts. - More context menu shortcuts.
- More keyboard shortcuts. - More keyboard shortcuts.
- Query logs console. - Query logs console.
- Fake data filler. - Fake data filler.
- Import/export and migration. - Import/export and migration.
- SSL and SSH tunnel support. - Light theme.
- Themes.
## Currently supported ## Currently supported
@@ -75,7 +74,7 @@ This is a roadmap with major features will come in near future.
- [x] Windows - [x] Windows
- [x] Linux - [x] Linux
- [x] MacOS (needs tests) - [x] MacOS (i need feedbacks)
#### • ARM #### • ARM
@@ -85,6 +84,10 @@ This is a roadmap with major features will come in near future.
## Translations ## Translations
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/Fabio286/antares/pull/20) **Italian Translation** (46%) / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/Fabio286/antares/pull/29) **Arabic Translation** (45%) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/Fabio286/antares/pull/32) **Spanish Translation** (46%) / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
## 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>

5
jsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"include": [
"./src/renderer/**/*"
]
}

View File

@@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.0.10", "version": "0.0.15",
"description": "A cross-platform easy to use SQL client.", "description": "A cross-platform easy to use SQL client.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/Fabio286/antares.git", "repository": "https://github.com/Fabio286/antares.git",
@@ -39,6 +39,10 @@
"AppImage" "AppImage"
], ],
"category": "Development" "category": "Development"
},
"appImage": {
"license": "./LICENSE",
"category": "Development"
} }
}, },
"electronWebpack": { "electronWebpack": {
@@ -48,11 +52,13 @@
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^5.8.55", "@mdi/font": "^5.8.55",
"ace-builds": "^1.4.12",
"electron-log": "^4.3.0", "electron-log": "^4.3.0",
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5", "electron-updater": "^4.3.5",
"keytar": "^7.3.0",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"moment": "^2.29.1", "moment": "^2.29.1",
"monaco-editor": "^0.20.0",
"mssql": "^6.2.3", "mssql": "^6.2.3",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"pg": "^8.5.1", "pg": "^8.5.1",
@@ -61,8 +67,7 @@
"vue-i18n": "^8.22.2", "vue-i18n": "^8.22.2",
"vue-the-mask": "^0.11.1", "vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.6.0", "vuex": "^3.6.0"
"vuex-persist": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
@@ -78,7 +83,6 @@
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-vue": "^7.1.0", "eslint-plugin-vue": "^7.1.0",
"monaco-editor-webpack-plugin": "^1.9.1",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"sass-loader": "^10.1.0", "sass-loader": "^10.1.0",
"standard-version": "^9.0.0", "standard-version": "^9.0.0",

View File

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

View File

@@ -1,12 +1,12 @@
export const TEXT = ['char', 'varchar']; export const TEXT = ['CHAR', 'VARCHAR'];
export const LONG_TEXT = ['text', 'mediumtext', 'longtext']; 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', 'FLOAT', 'DOUBLE', 'DECIMAL', 'BOOL'];
export const DATE = ['date']; export const DATE = ['DATE'];
export const TIME = ['time']; export const TIME = ['TIME'];
export const DATETIME = ['datetime', 'timestamp']; export const DATETIME = ['DATETIME', 'TIMESTAMP'];
export const BLOB = ['blob', 'mediumblob', 'longblob']; export const BLOB = ['BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB'];
export const BIT = ['bit']; export const BIT = ['BIT'];

View File

@@ -2,7 +2,9 @@
import { app, BrowserWindow, nativeImage } from 'electron'; import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path'; import * as path from 'path';
import crypto from 'crypto';
import { format as formatUrl } from 'url'; import { format as formatUrl } from 'url';
import keytar from 'keytar';
import ipcHandlers from './ipc-handlers'; import ipcHandlers from './ipc-handlers';
const isDevelopment = process.env.NODE_ENV !== 'production'; const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -89,7 +91,14 @@ else {
}); });
// create main BrowserWindow when electron is ready // create main BrowserWindow when electron is ready
app.on('ready', () => { app.on('ready', async () => {
let key = await keytar.getPassword('antares', 'user');
if (!key) {
key = crypto.randomBytes(16).toString('hex');
keytar.setPassword('antares', 'user', key);
}
mainWindow = createMainWindow(); mainWindow = createMainWindow();
}); });
} }

View File

@@ -1,7 +1,13 @@
import keytar from 'keytar';
import { app, ipcMain } from 'electron'; import { app, ipcMain } from 'electron';
export default () => { export default () => {
ipcMain.on('close-app', () => { ipcMain.on('close-app', () => {
app.exit(); app.exit();
}); });
ipcMain.on('get-key', async event => {
const key = await keytar.getPassword('antares', 'user');
event.returnValue = key;
});
}; };

View File

@@ -46,7 +46,7 @@ export default connections => {
try { try {
await connection.connect(); await connection.connect();
const structure = await connection.getStructure(); const structure = await connection.getStructure(new Set());
connections[conn.uid] = connection; connections[conn.uid] = connection;

View File

@@ -50,9 +50,9 @@ export default connections => {
} }
}); });
ipcMain.handle('get-structure', async (event, uid) => { ipcMain.handle('get-structure', async (event, params) => {
try { try {
const structure = await connections[uid].getStructure(); const structure = await connections[params.uid].getStructure(params.schemas);
return { status: 'success', response: structure }; return { status: 'success', response: structure };
} }

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

@@ -1,15 +1,27 @@
import connection from './connection'; import connection from './connection';
import tables from './tables'; 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 updates from './updates';
import application from './application'; import application from './application';
import database from './database'; import database from './database';
import users from './users';
const connections = {}; const connections = {};
export default () => { export default () => {
connection(connections); connection(connections);
tables(connections); tables(connections);
views(connections);
triggers(connections);
routines(connections);
functions(connections);
schedulers(connections);
database(connections); database(connections);
users(connections);
updates(); updates();
application(); application();
}; };

View File

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

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

@@ -14,14 +14,18 @@ export default (connections) => {
} }
}); });
ipcMain.handle('get-table-data', async (event, { uid, schema, table }) => { ipcMain.handle('get-table-data', async (event, { uid, schema, table, sortParams }) => {
try { try {
const result = await connections[uid] const query = connections[uid]
.select('*') .select('*')
.schema(schema) .schema(schema)
.from(table) .from(table)
.limit(1000) .limit(1000);
.run({ details: true });
if (sortParams && sortParams.field && sortParams.dir)
query.orderBy({ [sortParams.field]: sortParams.dir.toUpperCase() });
const result = await query.run({ details: true });
return { status: 'success', response: result }; return { status: 'success', response: result };
} }
@@ -89,11 +93,18 @@ export default (connections) => {
}); });
ipcMain.handle('delete-table-rows', async (event, params) => { ipcMain.handle('delete-table-rows', async (event, params) => {
let idString;
if (typeof params.rows[0] === 'string')
idString = params.rows.map(row => `"${row}"`).join(',');
else
idString = params.rows.join(',');
try { try {
const result = await connections[params.uid] const result = await connections[params.uid]
.schema(params.schema) .schema(params.schema)
.delete(params.table) .delete(params.table)
.where({ [params.primary]: `IN (${params.rows.join(',')})` }) .where({ [params.primary]: `IN (${idString})` })
.run(); .run();
return { status: 'success', response: result }; return { status: 'success', response: result };

View File

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

View File

@@ -0,0 +1,15 @@
import { ipcMain } from 'electron';
export default (connections) => {
ipcMain.handle('get-users', async (event, uid) => {
try {
const result = await connections[uid].getUsers();
return { status: 'success', response: result };
}
catch (err) {
if (err.code === 'ER_TABLEACCESS_DENIED_ERROR')
return { status: 'success', response: [] };
return { status: 'error', response: err.toString() };
}
});
};

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -69,75 +69,81 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.context { .context {
display: flex;
color: $body-font-color;
font-size: 16px;
z-index: 400;
justify-content: center;
align-items: center;
overflow: hidden;
position: fixed;
height: 100vh;
right: 0;
top: 0;
left: 0;
bottom: 0;
.context-container {
min-width: 100px;
z-index: 10;
box-shadow: 0 0 2px 0 #000;
padding: 0;
background: #1d1d1d;
border-radius: 0.1rem;
display: flex; display: flex;
color: $body-font-color; flex-direction: column;
font-size: 16px; position: absolute;
z-index: 400; pointer-events: initial;
justify-content: center;
align-items: center;
overflow: hidden;
position: fixed;
height: 100vh;
right: 0;
top: 0;
left: 0;
bottom: 0;
.context-container { .context-element {
min-width: 100px;
z-index: 10;
box-shadow: 0 0 2px 0 #000;
padding: 0;
background: #1d1d1d;
border-radius: 0.1rem;
display: flex; display: flex;
flex-direction: column; align-items: center;
position: absolute; padding: 0.1rem 0.3rem;
pointer-events: initial; cursor: pointer;
justify-content: space-between;
position: relative;
.context-element { .context-submenu {
display: flex; opacity: 0;
align-items: center; visibility: hidden;
padding: 0.1rem 0.3rem; transition: opacity 0.2s;
cursor: pointer; position: absolute;
justify-content: space-between; left: 100%;
position: relative; top: 0;
background: #1d1d1d;
box-shadow: 0 0 2px 0 #000;
min-width: 100px;
}
&:hover {
background: $primary-color;
.context-submenu { .context-submenu {
opacity: 0; display: block;
visibility: hidden; visibility: visible;
transition: opacity 0.2s; opacity: 1;
position: absolute;
left: 100%;
top: 0;
background: #1d1d1d;
box-shadow: 0 0 2px 0 #000;
min-width: 100px;
}
&:hover {
background: $primary-color;
.context-submenu {
display: block;
visibility: visible;
opacity: 1;
}
} }
} }
} }
.context-overlay {
background: transparent;
bottom: 0;
cursor: default;
display: block;
left: 0;
position: absolute;
right: 0;
top: 0;
}
} }
.context-overlay {
background: transparent;
bottom: 0;
cursor: default;
display: block;
left: 0;
position: absolute;
right: 0;
top: 0;
}
}
.disabled {
pointer-events: none;
filter: grayscale(100%);
opacity: 0.5;
}
</style> </style>

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

@@ -1,10 +1,14 @@
<template> <template>
<select <select
ref="editField" ref="editField"
class="px-1" class="form-select pl-1 pr-4"
:class="{'small-select': size === 'small'}"
@change="onChange" @change="onChange"
@blur="$emit('blur')" @blur="$emit('blur')"
> >
<option v-if="!isValidDefault" :value="value">
{{ value }} - {{ $t('message.invalidDefault') }}
</option>
<option <option
v-for="row in foreignList" v-for="row in foreignList"
:key="row.foreignColumn" :key="row.foreignColumn"
@@ -30,7 +34,11 @@ export default {
}, },
props: { props: {
value: [String, Number], value: [String, Number],
keyUsage: Object keyUsage: Object,
size: {
type: String,
default: ''
}
}, },
data () { data () {
return { return {
@@ -40,7 +48,10 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
selectedWorkspace: 'workspaces/getSelected' selectedWorkspace: 'workspaces/getSelected'
}) }),
isValidDefault () {
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString());
}
}, },
async created () { async created () {
let firstTextField; let firstTextField;
@@ -64,7 +75,7 @@ export default {
try { // Foregn list try { // Foregn list
const { status, response } = await Tables.getForeignList({ const { status, response } = await Tables.getForeignList({
...params, ...params,
column: this.keyUsage.refColumn, column: this.keyUsage.refField,
description: firstTextField description: firstTextField
}); });

View File

@@ -15,10 +15,11 @@
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.user') }}:</label> <label class="form-label">{{ $t('word.user') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
ref="firstInput"
v-model="credentials.user" v-model="credentials.user"
class="form-input" class="form-input"
type="text" type="text"
@@ -27,7 +28,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.password') }}:</label> <label class="form-label">{{ $t('word.password') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
@@ -63,6 +64,11 @@ export default {
} }
}; };
}, },
created () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: { methods: {
closeModal () { closeModal () {
this.$emit('close-asking'); this.$emit('close-asking');

View File

@@ -16,10 +16,11 @@
<fieldset class="m-0" :disabled="isTesting"> <fieldset class="m-0" :disabled="isTesting">
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}:</label> <label class="form-label">{{ $t('word.connectionName') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
ref="firstInput"
v-model="localConnection.name" v-model="localConnection.name"
class="form-input" class="form-input"
type="text" type="text"
@@ -28,7 +29,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}:</label> <label class="form-label">{{ $t('word.client') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<select v-model="localConnection.client" class="form-select"> <select v-model="localConnection.client" class="form-select">
@@ -52,7 +53,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP:</label> <label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
@@ -64,7 +65,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}:</label> <label class="form-label">{{ $t('word.port') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
@@ -78,7 +79,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}:</label> <label class="form-label">{{ $t('word.user') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
@@ -91,7 +92,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}:</label> <label class="form-label">{{ $t('word.password') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
@@ -172,6 +173,10 @@ export default {
created () { created () {
this.localConnection = Object.assign({}, this.connection); this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);

View File

@@ -15,7 +15,7 @@
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.name') }}:</label> <label class="form-label">{{ $t('word.name') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
@@ -30,10 +30,14 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.collation') }}:</label> <label class="form-label">{{ $t('word.collation') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<select v-model="database.collation" class="form-select"> <select
ref="firstInput"
v-model="database.collation"
class="form-select"
>
<option <option
v-for="collation in collations" v-for="collation in collations"
:key="collation.id" :key="collation.id"
@@ -114,6 +118,10 @@ export default {
}; };
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);

View File

@@ -16,10 +16,11 @@
<fieldset class="m-0" :disabled="isTesting"> <fieldset class="m-0" :disabled="isTesting">
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.connectionName') }}:</label> <label class="form-label">{{ $t('word.connectionName') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
ref="firstInput"
v-model="connection.name" v-model="connection.name"
class="form-input" class="form-input"
type="text" type="text"
@@ -28,7 +29,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.client') }}:</label> <label class="form-label">{{ $t('word.client') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<select <select
@@ -56,7 +57,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP:</label> <label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
@@ -68,7 +69,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}:</label> <label class="form-label">{{ $t('word.port') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
@@ -82,7 +83,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}:</label> <label class="form-label">{{ $t('word.user') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
@@ -95,7 +96,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}:</label> <label class="form-label">{{ $t('word.password') }}</label>
</div> </div>
<div class="col-8 col-sm-12"> <div class="col-8 col-sm-12">
<input <input
@@ -182,6 +183,10 @@ export default {
}, },
created () { created () {
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);

View File

@@ -15,10 +15,11 @@
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.name') }}:</label> <label class="form-label">{{ $t('word.name') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<input <input
ref="firstInput"
v-model="database.name" v-model="database.name"
class="form-input" class="form-input"
type="text" type="text"
@@ -29,7 +30,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.collation') }}:</label> <label class="form-label">{{ $t('word.collation') }}</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<select v-model="database.collation" class="form-select"> <select v-model="database.collation" class="form-select">
@@ -89,6 +90,9 @@ export default {
created () { created () {
this.database = { ...this.database, collation: this.defaultCollation }; this.database = { ...this.database, collation: this.defaultCollation };
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);

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,147 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewRoutine"
@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="localRoutine.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="localRoutine.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="localRoutine.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="localRoutine.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="localRoutine.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="localRoutine.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: 'ModalNewRoutine',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localRoutine: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
parameters: [],
name: '',
comment: '',
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
}
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewRoutine () {
this.$emit('open-create-routine-editor', this.localRoutine);
}
}
};
</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

@@ -18,6 +18,7 @@
</label> </label>
<div class="column"> <div class="column">
<input <input
ref="firstInput"
v-model="localOptions.name" v-model="localOptions.name"
class="form-input" class="form-input"
type="text" type="text"
@@ -83,7 +84,6 @@ export default {
ConfirmModal ConfirmModal
}, },
props: { props: {
table: String,
workspace: Object workspace: Object
}, },
data () { data () {
@@ -112,6 +112,10 @@ export default {
mounted () { mounted () {
this.localOptions.collation = this.defaultCollation; this.localOptions.collation = this.defaultCollation;
this.localOptions.engine = this.defaultEngine; this.localOptions.engine = this.defaultEngine;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
methods: { methods: {
confirmOptionsChange () { confirmOptionsChange () {

View File

@@ -25,6 +25,7 @@
<div class="input-group col-8 col-sm-12"> <div class="input-group col-8 col-sm-12">
<ForeignKeySelect <ForeignKeySelect
v-if="foreignKeys.includes(field.name)" v-if="foreignKeys.includes(field.name)"
ref="formInput"
class="form-select" class="form-select"
:value.sync="localRow[field.name]" :value.sync="localRow[field.name]"
:key-usage="getKeyUsage(field.name)" :key-usage="getKeyUsage(field.name)"
@@ -32,6 +33,7 @@
/> />
<input <input
v-else-if="inputProps(field).mask" v-else-if="inputProps(field).mask"
ref="formInput"
v-model="localRow[field.name]" v-model="localRow[field.name]"
v-mask="inputProps(field).mask" v-mask="inputProps(field).mask"
class="form-input" class="form-input"
@@ -41,6 +43,7 @@
> >
<input <input
v-else-if="inputProps(field).type === 'file'" v-else-if="inputProps(field).type === 'file'"
ref="formInput"
class="form-input" class="form-input"
type="file" type="file"
:disabled="fieldsToExclude.includes(field.name)" :disabled="fieldsToExclude.includes(field.name)"
@@ -49,13 +52,14 @@
> >
<input <input
v-else v-else
ref="formInput"
v-model="localRow[field.name]" v-model="localRow[field.name]"
class="form-input" class="form-input"
:type="inputProps(field).type" :type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)" :disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1" :tabindex="key+1"
> >
<span class="input-group-addon" :class="`type-${field.type}`"> <span class="input-group-addon" :class="`type-${field.type.toLowerCase()}`">
{{ field.type }} {{ fieldLength(field) | wrapNumber }} {{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span> </span>
<label class="form-checkbox ml-3" :title="$t('word.insert')"> <label class="form-checkbox ml-3" :title="$t('word.insert')">
@@ -146,7 +150,7 @@ export default {
return this.getWorkspace(this.selectedWorkspace); return this.getWorkspace(this.selectedWorkspace);
}, },
foreignKeys () { foreignKeys () {
return this.keyUsage.map(key => key.column); return this.keyUsage.map(key => key.field);
} }
}, },
watch: { watch: {
@@ -192,6 +196,12 @@ export default {
} }
this.localRow = { ...rowObj }; this.localRow = { ...rowObj };
// Auto focus
setTimeout(() => {
const firstSelectableInput = this.$refs.formInput.find(input => !input.disabled);
firstSelectableInput.focus();
}, 20);
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
@@ -243,7 +253,7 @@ export default {
}, },
fieldLength (field) { fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null; if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numLength || field.datePrecision || field.charLength || 0; return field.length;
}, },
inputProps (field) { inputProps (field) {
if ([...TEXT, ...LONG_TEXT].includes(field.type)) if ([...TEXT, ...LONG_TEXT].includes(field.type))
@@ -296,7 +306,7 @@ export default {
this.localRow[field] = files[0].path; this.localRow[field] = files[0].path;
}, },
getKeyUsage (keyName) { getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName); return this.keyUsage.find(key => key.field === keyName);
}, },
onKey (e) { onKey (e) {
e.stopPropagation(); e.stopPropagation();

View File

@@ -0,0 +1,140 @@
<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.createNewTrigger') }}
</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="localTrigger.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="localTrigger.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.table') }}
</label>
<div class="column">
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.event') }}
</label>
<div class="column">
<div class="input-group">
<select v-model="localTrigger.event1" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select v-model="localTrigger.event2" class="form-select">
<option>INSERT</option>
<option>UPDATE</option>
<option>DELETE</option>
</select>
</div>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewTrigger',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localTrigger: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
table: '',
event1: 'BEFORE',
event2: 'INSERT'
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
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') : [];
}
},
mounted () {
this.localTrigger.table = this.schemaTables.length ? this.schemaTables[0].name : '';
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewTrigger () {
this.$emit('open-create-trigger-editor', this.localTrigger);
}
}
};
</script>

View File

@@ -0,0 +1,192 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmOptionsChange"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-eye-plus mr-1" /> {{ $t('message.createNewView') }}
</div>
</template>
<div :slot="'body'">
<div class="container">
<div class="columns mb-4">
<div class="column col-6">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-6">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select v-model="localView.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>
</div>
</div>
</div>
<div class="columns">
<div class="column col-4">
<div class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="DEFINER"
>
<i class="form-icon" /> DEFINER
</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="INVOKER"
>
<i class="form-icon" /> INVOKER
</label>
</div>
</div>
<div class="column col-4">
<div class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
name="algorithm"
value="UNDEFINED"
>
<i class="form-icon" /> UNDEFINED
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="MERGE"
name="algorithm"
>
<i class="form-icon" /> MERGE
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="TEMPTABLE"
name="algorithm"
>
<i class="form-icon" /> TEMPTABLE
</label>
</div>
</div>
<div class="column col-4">
<div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value=""
>
<i class="form-icon" /> None
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="CASCADED"
>
<i class="form-icon" /> CASCADED
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="LOCAL"
>
<i class="form-icon" /> LOCAL
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label>
<QueryEditor
ref="queryEditor"
:value.sync="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import QueryEditor from '@/components/QueryEditor';
export default {
name: 'ModalNewView',
components: {
ConfirmModal,
QueryEditor
},
props: {
workspace: Object
},
data () {
return {
localView: {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
},
isOptionsChanging: false,
editorHeight: 300
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
}
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
this.$emit('open-create-view-editor', this.localView);
}
}
};
</script>

View File

@@ -45,59 +45,154 @@
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="selectedTab === 'general'" class="panel-body py-4"> <div v-if="selectedTab === 'general'" class="panel-body py-4">
<form class="form-horizontal"> <div class="container">
<div class="col-8 col-sm-12"> <form class="form-horizontal columns">
<div class="form-group mb-4"> <div class="column col-12 h6 text-uppercase mb-1">
<div class="col-6 col-sm-12"> {{ $t('word.application') }}
<label class="form-label"> </div>
<i class="mdi mdi-18px mdi-translate mr-1" /> <div class="column col-8 col-sm-12 mb-2">
{{ $t('word.language') }}: <div class="form-group mb-4">
</label> <div class="col-6 col-sm-12">
</div> <label class="form-label">
<div class="col-6 col-sm-12"> <i class="mdi mdi-18px mdi-translate mr-1" />
<select {{ $t('word.language') }}
v-model="localLocale" </label>
class="form-select" </div>
@change="changeLocale(localLocale)" <div class="col-6 col-sm-12">
> <select
<option v-model="localLocale"
v-for="(locale, key) in locales" class="form-select"
:key="key" @change="changeLocale(localLocale)"
:value="locale.code"
> >
{{ locale.name }} <option
</option> v-for="(locale, key) in locales"
</select> :key="key"
:value="locale.code"
>
{{ locale.name }}
</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
{{ $t('message.notificationsTimeout') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="input-group">
<input
v-model="localTimeout"
class="form-input"
type="number"
min="1"
@focusout="checkNotificationsTimeout"
>
<span class="input-group-addon">{{ $t('word.seconds') }}</span>
</div>
</div>
</div> </div>
</div> </div>
<div class="form-group">
<div class="col-6 col-sm-12"> <div class="column col-12 h6 mt-4 text-uppercase mb-1">
<label class="form-label"> {{ $t('word.editor') }}
{{ $t('message.notificationsTimeout') }}: </div>
</label> <div class="column col-8 col-sm-12">
<div class="form-group">
<div class="col-6 col-sm-12">
<label class="form-label">
{{ $t('word.autoCompletion') }}
</label>
</div>
<div class="col-6 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleAutoComplete">
<input type="checkbox" :checked="selectedAutoComplete">
<i class="form-icon" />
</label>
</div>
</div> </div>
<div class="col-6 col-sm-12"> </div>
<div class="input-group"> <div class="column col-8 col-sm-12">
<input <div class="form-group">
v-model="localTimeout" <div class="col-6 col-sm-12">
class="form-input" <label class="form-label">
type="number" {{ $t('message.wrapLongLines') }}
min="1" </label>
@focusout="checkNotificationsTimeout" </div>
> <div class="col-6 col-sm-12">
<span class="input-group-addon">{{ $t('word.seconds') }}</span> <label class="form-switch d-inline-block" @click.prevent="toggleLineWrap">
<input type="checkbox" :checked="selectedLineWrap">
<i class="form-icon" />
</label>
</div>
</div>
</div>
</form>
</div>
</div>
<div v-if="selectedTab === 'themes'" class="panel-body py-4">
<div class="container">
<div class="columns">
<div class="column col-12 h6 text-uppercase mb-2">
{{ $t('message.applicationTheme') }}
</div>
<div class="column col-6 c-hand theme-block" :class="{'selected': applicationTheme === 'dark'}">
<img :src="require('@/images/dark.png').default" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name">
<i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.dark') }}
</div>
</div>
</div>
<div class="column col-6 theme-block disabled" :class="{'selected': applicationTheme === 'light'}">
<div class="theme-name">
<i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.light') }} (Coming)
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form>
</div>
<div v-if="selectedTab === 'themes'" class="panel-body py-4"> <div class="columns mt-4">
<div class="text-center"> <div class="column col-12 h6 text-uppercase mb-2 mt-4">
<p>In future releases</p> {{ $t('message.editorTheme') }}
</div>
<div class="column col-6 h5 mb-4">
<select
v-model="localEditorTheme"
class="form-select"
@change="changeEditorTheme(localEditorTheme)"
>
<optgroup
v-for="group in editorThemes"
:key="group.group"
:label="group.group"
>
<option
v-for="theme in group.themes"
:key="theme.name"
:value="theme.code"
:selected="editorTheme === theme.code"
>
{{ theme.name }}
</option>
</optgroup>
</select>
</div>
<div class="column col-12">
<QueryEditor
:value="exampleQuery"
:workspace="workspace"
:read-only="true"
:height="270"
/>
</div>
</div>
</div> </div>
</div> </div>
@@ -110,8 +205,9 @@
<img :src="require('@/images/logo.svg').default" width="128"> <img :src="require('@/images/logo.svg').default" width="128">
<h4>{{ appName }}</h4> <h4>{{ appName }}</h4>
<p> <p>
{{ $t('word.version') }}: {{ appVersion }}<br> {{ $t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/EStarium/antares')">GitHub</a><br> <a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a> | <a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares/blob/master/CHANGELOG.md')">CHANGELOG</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
<small>{{ $t('message.madeWithJS') }}</small> <small>{{ $t('message.madeWithJS') }}</small>
</p> </p>
</div> </div>
@@ -126,18 +222,70 @@
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import localesNames from '@/i18n/supported-locales'; import localesNames from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate'; import ModalSettingsUpdate from '@/components/ModalSettingsUpdate';
import QueryEditor from '@/components/QueryEditor';
const { shell } = require('electron'); const { shell } = require('electron');
export default { export default {
name: 'ModalSettings', name: 'ModalSettings',
components: { components: {
ModalSettingsUpdate ModalSettingsUpdate,
QueryEditor
}, },
data () { data () {
return { return {
localLocale: null, localLocale: null,
localTimeout: null, localTimeout: null,
selectedTab: 'general' localEditorTheme: null,
selectedTab: 'general',
editorThemes: [
{
group: this.$t('word.light'),
themes: [
{ code: 'chrome', name: 'Chrome' },
{ code: 'clouds', name: 'Clouds' },
{ code: 'crimson_editor', name: 'Crimson Editor' },
{ code: 'dawn', name: 'Dawn' },
{ code: 'dreamweaver', name: 'Dreamweaver' },
{ code: 'eclupse', name: 'Eclipse' },
{ code: 'github', name: 'GitHub' },
{ code: 'iplastic', name: 'IPlastic' },
{ code: 'solarized_light', name: 'Solarized Light' },
{ code: 'textmate', name: 'TextMate' },
{ code: 'tomorrow', name: 'Tomorrow' },
{ code: 'xcode', name: 'Xcode' },
{ code: 'kuroir', name: 'Kuroir' },
{ code: 'katzenmilch', name: 'KatzenMilch' },
{ code: 'sqlserver', name: 'SQL Server' }
]
},
{
group: this.$t('word.dark'),
themes: [
{ code: 'ambiance', name: 'Ambiance' },
{ code: 'chaos', name: 'Chaos' },
{ code: 'clouds_midnight', name: 'Clouds Midnight' },
{ code: 'dracula', name: 'Dracula' },
{ code: 'cobalt', name: 'Cobalt' },
{ code: 'gruvbox', name: 'Gruvbox' },
{ code: 'gob', name: 'Green on Black' },
{ code: 'idle_fingers', name: 'Idle Fingers' },
{ code: 'kr_theme', name: 'krTheme' },
{ code: 'merbivore', name: 'Merbivore' },
{ code: 'mono_industrial', name: 'Mono Industrial' },
{ code: 'monokai', name: 'Monokai' },
{ code: 'nord_dark', name: 'Nord Dark' },
{ code: 'pastel_on_dark', name: 'Pastel on Dark' },
{ code: 'solarized_dark', name: 'Solarized Dark' },
{ code: 'terminal', name: 'Terminal' },
{ code: 'tomorrow_night', name: 'Tomorrow Night' },
{ code: 'tomorrow_night_blue', name: 'Tomorrow Night Blue' },
{ code: 'tomorrow_night_bright', name: 'Tomorrow Night Bright' },
{ code: 'tomorrow_night_eighties', name: 'Tomorrow Night 80s' },
{ code: 'twilight', name: 'Twilight' },
{ code: 'vibrant_ink', name: 'Vibrant Ink' }
]
}
]
}; };
}, },
computed: { computed: {
@@ -146,8 +294,14 @@ export default {
appVersion: 'application/appVersion', appVersion: 'application/appVersion',
selectedSettingTab: 'application/selectedSettingTab', selectedSettingTab: 'application/selectedSettingTab',
selectedLocale: 'settings/getLocale', selectedLocale: 'settings/getLocale',
selectedAutoComplete: 'settings/getAutoComplete',
selectedLineWrap: 'settings/getLineWrap',
notificationsTimeout: 'settings/getNotificationsTimeout', notificationsTimeout: 'settings/getNotificationsTimeout',
updateStatus: 'application/getUpdateStatus' applicationTheme: 'settings/getApplicationTheme',
editorTheme: 'settings/getEditorTheme',
updateStatus: 'application/getUpdateStatus',
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}), }),
locales () { locales () {
const locales = []; const locales = [];
@@ -158,11 +312,32 @@ export default {
}, },
hasUpdates () { hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus); return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
},
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
exampleQuery () {
return `-- This is an example
SELECT
employee.id,
employee.first_name,
employee.last_name,
SUM(DATEDIFF("SECOND", call.start, call.end)) AS call_duration
FROM call
INNER JOIN employee ON call.employee_id = employee.id
GROUP BY
employee.id,
employee.first_name,
employee.last_name
ORDER BY
employee.id ASC;
`;
} }
}, },
created () { created () {
this.localLocale = this.selectedLocale; this.localLocale = this.selectedLocale;
this.localTimeout = this.notificationsTimeout; this.localTimeout = this.notificationsTimeout;
this.localEditorTheme = this.editorTheme;
this.selectedTab = this.selectedSettingTab; this.selectedTab = this.selectedSettingTab;
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
}, },
@@ -173,6 +348,9 @@ export default {
...mapActions({ ...mapActions({
closeModal: 'application/hideSettingModal', closeModal: 'application/hideSettingModal',
changeLocale: 'settings/changeLocale', changeLocale: 'settings/changeLocale',
changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap',
changeEditorTheme: 'settings/changeEditorTheme',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout' updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}), }),
selectTab (tab) { selectTab (tab) {
@@ -191,6 +369,12 @@ export default {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') if (e.key === 'Escape')
this.closeModal(); this.closeModal();
},
toggleAutoComplete () {
this.changeAutoComplete(!this.selectedAutoComplete);
},
toggleLineWrap () {
this.changeLineWrap(!this.selectedLineWrap);
} }
} }
}; };
@@ -204,6 +388,34 @@ export default {
.panel-body { .panel-body {
height: calc(70vh - 70px); height: calc(70vh - 70px);
overflow: auto; overflow: auto;
.theme-block {
position: relative;
text-align: center;
&.selected {
img {
box-shadow: 0 0 0 3px $primary-color;
}
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.theme-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
top: 0;
height: 100%;
width: 100%;
text-shadow: 0 0 8px #000;
}
}
} }
.badge::after { .badge::after {

View File

@@ -1,70 +1,282 @@
<template> <template>
<div class="editor-wrapper"> <div class="editor-wrapper">
<div ref="editor" class="editor" /> <div
ref="editor"
class="editor"
:style="{height: `${height}px`}"
/>
</div> </div>
</template> </template>
<script> <script>
import * as ace from 'ace-builds';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import 'ace-builds/webpack-resolver';
import { completionItemProvider } from '@/suggestions/sql'; import '../libs/ext-language_tools';
import { mapGetters } from 'vuex';
monaco.languages.registerCompletionItemProvider('sql', completionItemProvider(monaco)); import Tables from '@/ipc-api/Tables';
export default { export default {
name: 'QueryEditor', name: 'QueryEditor',
props: { props: {
value: String value: String,
workspace: Object,
schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
height: { type: Number, default: 200 }
}, },
data () { data () {
return { return {
editor: null editor: null,
fields: [],
baseCompleter: []
}; };
}, },
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
}),
tables () {
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema)
.reduce((acc, curr) => {
acc.push(...curr.tables);
return acc;
}, []).map(table => {
return {
name: table.name,
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':
case 'maria':
return 'mysql';
case 'mssql':
return 'sqlserver';
case 'pg':
return 'pgsql';
default:
return 'sql';
}
},
lastWord () {
const words = this.value.split(' ');
return words[words.length - 1];
},
isLastWordATable () {
return /\w+\.\w*/gm.test(this.lastWord);
},
fieldsCompleter () {
return {
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
this.fields.forEach(field => {
completions.push({
value: field,
meta: 'column',
score: 1000
});
});
callback(null, completions);
}
};
}
},
watch: {
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
});
}
}
},
mounted () { mounted () {
this.editor = monaco.editor.create(this.$refs.editor, { this.editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value, value: this.value,
language: 'sql', fontSize: '14px',
theme: 'vs-dark', printMargin: false,
autoIndent: true, readOnly: this.readOnly
minimap: {
enabled: false
},
contextmenu: false,
wordBasedSuggestions: true,
acceptSuggestionOnEnter: 'smart',
quickSuggestions: true
}); });
this.editor.onDidChangeModelContent(e => { this.editor.setOptions({
enableBasicAutocompletion: true,
wrap: this.lineWrap,
enableSnippets: true,
enableLiveAutocompletion: this.autoComplete
});
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.baseCompleter = this.editor.completers;
this.editor.commands.on('afterExec', e => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
if (this.isLastWordATable || e.args === '.') {
if (e.args !== ' ') {
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop());
if (table) {
const params = {
uid: this.workspace.uid,
schema: this.schema,
table: table.name
};
Tables.getTableColumns(params).then(res => {
if (res.response.length)
this.fields = res.response.map(field => field.name);
this.editor.completers = [this.fieldsCompleter];
this.editor.execCommand('startAutocomplete');
}).catch(console.log);
}
else
this.editor.completers = this.baseCompleter;
}
else
this.editor.completers = this.baseCompleter;
}
else
this.editor.completers = this.baseCompleter;
}
});
this.editor.session.on('change', () => {
const content = this.editor.getValue(); const content = this.editor.getValue();
this.$emit('update:value', content); this.$emit('update:value', content);
}); });
},
beforeDestroy () { if (this.autoFocus) {
this.editor && this.editor.dispose(); setTimeout(() => {
this.editor.focus();
this.editor.resize();
}, 20);
}
setTimeout(() => {
this.editor.resize();
}, 20);
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.editor-wrapper { .editor-wrapper {
border-bottom: 1px solid #444; border-bottom: 1px solid #444;
.editor { .editor {
height: 200px; width: 100%;
width: 100%;
}
} }
}
.CodeMirror { .ace_.mdi {
.CodeMirror-scroll { display: inline-block;
max-width: 100%; width: 17px;
} }
.CodeMirror-line { .ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
word-break: break-word !important; background-color: #c9561a99;
white-space: pre-wrap !important; }
}
} .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> </style>

View File

@@ -15,7 +15,7 @@
<i class="mdi mdi-18px mdi-coffee mr-1" /> <i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small> <small>{{ $t('word.donate') }}</small>
</li> </li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/EStarium/antares/issues')"> <li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
<i class="mdi mdi-18px mdi-bug" /> <i class="mdi mdi-18px mdi-bug" />
</li> </li>
<li class="footer-element footer-link" @click="showSettingModal('about')"> <li class="footer-element footer-link" @click="showSettingModal('about')">

View File

@@ -13,25 +13,25 @@
</a> </a>
</li> </li>
<li <li
v-if="workspace.breadcrumbs.table" v-if="schemaChild"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'prop'}" :class="{'active': selectedTab === 'prop'}"
@click="selectTab({uid: workspace.uid, tab: 'prop'})" @click="selectTab({uid: workspace.uid, tab: 'prop'})"
> >
<a class="tab-link"> <a class="tab-link">
<i class="mdi mdi-18px mdi-tune mr-1" /> <i class="mdi mdi-18px mdi-tune mr-1" />
<span :title="workspace.breadcrumbs.table">{{ $t('word.properties').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span> <span :title="schemaChild">{{ $t('word.properties').toUpperCase() }}: {{ schemaChild }}</span>
</a> </a>
</li> </li>
<li <li
v-if="workspace.breadcrumbs.table" v-if="workspace.breadcrumbs.table || workspace.breadcrumbs.view"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'data'}" :class="{'active': selectedTab === 'data'}"
@click="selectTab({uid: workspace.uid, tab: 'data'})" @click="selectTab({uid: workspace.uid, tab: 'data'})"
> >
<a class="tab-link"> <a class="tab-link">
<i class="mdi mdi-18px mdi-table mr-1" /> <i class="mdi mdi-18px mr-1" :class="workspace.breadcrumbs.table ? 'mdi-table' : 'mdi-table-eye'" />
<span :title="workspace.breadcrumbs.table">{{ $t('word.data').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span> <span :title="schemaChild">{{ $t('word.data').toUpperCase() }}: {{ schemaChild }}</span>
</a> </a>
</li> </li>
<li <li
@@ -66,15 +66,45 @@
</li> </li>
</ul> </ul>
<WorkspacePropsTab <WorkspacePropsTab
v-show="selectedTab === 'prop'" v-show="selectedTab === 'prop' && workspace.breadcrumbs.table"
:is-selected="selectedTab === 'prop'" :is-selected="selectedTab === 'prop'"
:connection="connection" :connection="connection"
:table="workspace.breadcrumbs.table" :table="workspace.breadcrumbs.table"
/> />
<WorkspacePropsTabView
v-show="selectedTab === 'prop' && workspace.breadcrumbs.view"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:view="workspace.breadcrumbs.view"
/>
<WorkspacePropsTabTrigger
v-show="selectedTab === 'prop' && workspace.breadcrumbs.trigger"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:trigger="workspace.breadcrumbs.trigger"
/>
<WorkspacePropsTabRoutine
v-show="selectedTab === 'prop' && workspace.breadcrumbs.procedure"
:is-selected="selectedTab === 'prop'"
: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 <WorkspaceTableTab
v-show="selectedTab === 'data'" v-show="selectedTab === 'data'"
:connection="connection" :connection="connection"
:table="workspace.breadcrumbs.table" :table="workspace.breadcrumbs.table || workspace.breadcrumbs.view"
/> />
<WorkspaceQueryTab <WorkspaceQueryTab
v-for="tab of queryTabs" v-for="tab of queryTabs"
@@ -94,6 +124,11 @@ import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
import WorkspaceQueryTab from '@/components/WorkspaceQueryTab'; import WorkspaceQueryTab from '@/components/WorkspaceQueryTab';
import WorkspaceTableTab from '@/components/WorkspaceTableTab'; import WorkspaceTableTab from '@/components/WorkspaceTableTab';
import WorkspacePropsTab from '@/components/WorkspacePropsTab'; 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 { export default {
name: 'Workspace', name: 'Workspace',
@@ -101,7 +136,12 @@ export default {
WorkspaceExploreBar, WorkspaceExploreBar,
WorkspaceQueryTab, WorkspaceQueryTab,
WorkspaceTableTab, WorkspaceTableTab,
WorkspacePropsTab WorkspacePropsTab,
WorkspacePropsTabView,
WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabScheduler
}, },
props: { props: {
connection: Object connection: Object
@@ -123,7 +163,15 @@ export default {
return this.selectedWorkspace === this.connection.uid; return this.selectedWorkspace === this.connection.uid;
}, },
selectedTab () { selectedTab () {
if (this.workspace.breadcrumbs.table === null) 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)
)
return this.queryTabs[0].uid; return this.queryTabs[0].uid;
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) || return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) ||
@@ -133,6 +181,13 @@ export default {
}, },
queryTabs () { queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query'); return this.workspace.tabs.filter(tab => tab.type === 'query');
},
schemaChild () {
for (const key in this.workspace.breadcrumbs) {
if (key === 'schema') continue;
if (this.workspace.breadcrumbs[key]) return this.workspace.breadcrumbs[key];
}
return false;
} }
}, },
async created () { async created () {
@@ -193,6 +248,7 @@ export default {
align-items: flex-start; align-items: flex-start;
flex-wrap: nowrap; flex-wrap: nowrap;
overflow: auto; overflow: auto;
margin-bottom: 0;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 2px; width: 2px;

View File

@@ -40,6 +40,7 @@
:connection="connection" :connection="connection"
@show-database-context="openDatabaseContext" @show-database-context="openDatabaseContext"
@show-table-context="openTableContext" @show-table-context="openTableContext"
@show-misc-context="openMiscContext"
/> />
</div> </div>
</div> </div>
@@ -54,12 +55,47 @@
@close="hideCreateTableModal" @close="hideCreateTableModal"
@open-create-table-editor="openCreateTableEditor" @open-create-table-editor="openCreateTableEditor"
/> />
<ModalNewView
v-if="isNewViewModal"
:workspace="workspace"
@close="hideCreateViewModal"
@open-create-view-editor="openCreateViewEditor"
/>
<ModalNewTrigger
v-if="isNewTriggerModal"
:workspace="workspace"
@close="hideCreateTriggerModal"
@open-create-trigger-editor="openCreateTriggerEditor"
/>
<ModalNewRoutine
v-if="isNewRoutineModal"
:workspace="workspace"
@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 <DatabaseContext
v-if="isDatabaseContext" v-if="isDatabaseContext"
:selected-database="selectedDatabase" :selected-database="selectedDatabase"
:context-event="databaseContextEvent" :context-event="databaseContextEvent"
@close-context="closeDatabaseContext" @close-context="closeDatabaseContext"
@show-create-table-modal="showCreateTableModal" @show-create-table-modal="showCreateTableModal"
@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" @reload="refresh"
/> />
<TableContext <TableContext
@@ -69,19 +105,39 @@
@close-context="closeTableContext" @close-context="closeTableContext"
@reload="refresh" @reload="refresh"
/> />
<MiscContext
v-if="isMiscContext"
:selected-misc="selectedMisc"
:context-event="miscContextEvent"
@close-context="closeMiscContext"
@reload="refresh"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import _ from 'lodash'; import _ from 'lodash';// TODO: remove
import Tables from '@/ipc-api/Tables'; 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 WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase'; import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase';
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext'; import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext';
import TableContext from '@/components/WorkspaceExploreBarTableContext'; import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import ModalNewDatabase from '@/components/ModalNewDatabase'; import ModalNewDatabase from '@/components/ModalNewDatabase';
import ModalNewTable from '@/components/ModalNewTable'; 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 { export default {
name: 'WorkspaceExploreBar', name: 'WorkspaceExploreBar',
@@ -90,8 +146,14 @@ export default {
WorkspaceExploreBarDatabase, WorkspaceExploreBarDatabase,
DatabaseContext, DatabaseContext,
TableContext, TableContext,
MiscContext,
ModalNewDatabase, ModalNewDatabase,
ModalNewTable ModalNewTable,
ModalNewView,
ModalNewTrigger,
ModalNewRoutine,
ModalNewFunction,
ModalNewScheduler
}, },
props: { props: {
connection: Object, connection: Object,
@@ -100,15 +162,27 @@ export default {
data () { data () {
return { return {
isRefreshing: false, isRefreshing: false,
isNewDBModal: false, isNewDBModal: false,
isNewTableModal: false, isNewTableModal: false,
isNewViewModal: false,
isNewTriggerModal: false,
isNewRoutineModal: false,
isNewFunctionModal: false,
isNewSchedulerModal: false,
localWidth: null, localWidth: null,
isDatabaseContext: false, isDatabaseContext: false,
isTableContext: false, isTableContext: false,
isMiscContext: false,
databaseContextEvent: null, databaseContextEvent: null,
tableContextEvent: null, tableContextEvent: null,
miscContextEvent: null,
selectedDatabase: '', selectedDatabase: '',
selectedTable: '' selectedTable: null,
selectedMisc: null
}; };
}, },
computed: { computed: {
@@ -215,6 +289,129 @@ export default {
}, },
closeTableContext () { closeTableContext () {
this.isTableContext = false; this.isTableContext = false;
},
openMiscContext (payload) {
this.selectedMisc = payload.misc;
this.miscContextEvent = payload.event;
this.isMiscContext = true;
},
closeMiscContext () {
this.isMiscContext = false;
},
showCreateViewModal () {
this.closeDatabaseContext();
this.isNewViewModal = true;
},
hideCreateViewModal () {
this.isNewViewModal = false;
},
async openCreateViewEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Views.createView(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, view: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateTriggerModal () {
this.closeDatabaseContext();
this.isNewTriggerModal = true;
},
hideCreateTriggerModal () {
this.isNewTriggerModal = false;
},
async openCreateTriggerEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Triggers.createTrigger(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, trigger: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateRoutineModal () {
this.closeDatabaseContext();
this.isNewRoutineModal = true;
},
hideCreateRoutineModal () {
this.isNewRoutineModal = false;
},
async openCreateRoutineEditor (payload) {
const params = {
uid: this.connection.uid,
...payload
};
const { status, response } = await Routines.createRoutine(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedDatabase, procedure: payload.name });
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
}
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 });
} }
} }
}; };

View File

@@ -3,10 +3,11 @@
<summary <summary
class="accordion-header database-name" class="accordion-header database-name"
:class="{'text-bold': breadcrumbs.schema === database.name}" :class="{'text-bold': breadcrumbs.schema === database.name}"
@click="changeBreadcrumbs({schema: database.name, table: null})" @click="selectSchema(database.name)"
@contextmenu.prevent="showDatabaseContext($event, database.name)" @contextmenu.prevent="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" /> <i class="database-icon mdi mdi-18px mdi-database mr-1" />
<span>{{ database.name }}</span> <span>{{ database.name }}</span>
</summary> </summary>
@@ -17,20 +18,136 @@
v-for="table of database.tables" v-for="table of database.tables"
:key="table.name" :key="table.name"
class="menu-item" class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.name}" :class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@click="setBreadcrumbs({schema: database.name, table: table.name})" @click="setBreadcrumbs({schema: database.name, [table.type]: table.name})"
@contextmenu.prevent="showTableContext($event, table.name)" @contextmenu.prevent="showTableContext($event, table)"
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" /> <i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span>{{ table.name }}</span> <span>{{ table.name }}</span>
</a> </a>
<div class="table-size tooltip tooltip-left mr-1" :data-tooltip="formatBytes(table.size)"> <div
v-if="table.type === 'table'"
class="table-size tooltip tooltip-left mr-1"
:data-tooltip="formatBytes(table.size)"
>
<div class="pie" :style="piePercentage(table.size)" /> <div class="pie" :style="piePercentage(table.size)" />
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
<div v-if="database.triggers.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" />
{{ $tc('word.trigger', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="trigger of database.triggers"
:key="trigger.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@click="setBreadcrumbs({schema: database.name, trigger: trigger.name})"
@contextmenu.prevent="showMiscContext($event, {...trigger, type: 'trigger'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<span>{{ trigger.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="database.procedures.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" />
{{ $tc('word.storedRoutine', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="procedure of database.procedures"
:key="procedure.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@click="setBreadcrumbs({schema: database.name, procedure: procedure.name})"
@contextmenu.prevent="showMiscContext($event, {...procedure, type: 'procedure'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span>{{ procedure.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="database.functions.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" />
{{ $tc('word.function', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="func of database.functions"
:key="func.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@click="setBreadcrumbs({schema: database.name, function: func.name})"
@contextmenu.prevent="showMiscContext($event, {...func, type: 'function'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" />
<span>{{ func.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="database.schedulers.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" />
{{ $tc('word.scheduler', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="scheduler of database.schedulers"
:key="scheduler.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@click="setBreadcrumbs({schema: database.name, scheduler: scheduler.name})"
@contextmenu.prevent="showMiscContext($event, {...scheduler, type: 'scheduler'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<span>{{ scheduler.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
</div> </div>
</details> </details>
</template> </template>
@@ -45,13 +162,22 @@ export default {
database: Object, database: Object,
connection: Object connection: Object
}, },
data () {
return {
isLoading: false
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
getLoadedSchemas: 'workspaces/getLoadedSchemas',
getWorkspace: 'workspaces/getWorkspace' getWorkspace: 'workspaces/getWorkspace'
}), }),
breadcrumbs () { breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs; return this.getWorkspace(this.connection.uid).breadcrumbs;
}, },
loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid);
},
maxSize () { maxSize () {
return this.database.tables.reduce((acc, curr) => { return this.database.tables.reduce((acc, curr) => {
if (curr.size > acc) acc = curr.size; if (curr.size > acc) acc = curr.size;
@@ -64,15 +190,31 @@ export default {
}, },
methods: { methods: {
...mapActions({ ...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs' changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
refreshSchema: 'workspaces/refreshSchema'
}), }),
formatBytes, 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) { showDatabaseContext (event, database) {
this.changeBreadcrumbs({ schema: database, table: null });
this.$emit('show-database-context', { event, database }); this.$emit('show-database-context', { event, database });
}, },
showTableContext (event, table) { showTableContext (event, table) {
this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name });
this.$emit('show-table-context', { event, table }); this.$emit('show-table-context', { event, table });
}, },
showMiscContext (event, misc) {
this.setBreadcrumbs({ schema: this.database.name, [misc.type]: misc.name });
this.$emit('show-misc-context', { event, misc });
},
piePercentage (val) { piePercentage (val) {
const perc = val / this.maxSize * 100; const perc = val / this.maxSize * 100;
return { background: `conic-gradient(lime ${perc}%, white 0)` }; return { background: `conic-gradient(lime ${perc}%, white 0)` };
@@ -88,6 +230,7 @@ export default {
<style lang="scss"> <style lang="scss">
.workspace-explorebar-database { .workspace-explorebar-database {
.database-name, .database-name,
.misc-name,
a.table-name { a.table-name {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -103,12 +246,30 @@ export default {
} }
.database-icon, .database-icon,
.table-icon { .table-icon,
.misc-icon {
opacity: 0.7; opacity: 0.7;
} }
.loading {
height: 18px;
width: 18px;
&::after {
height: 0.6rem;
width: 0.6rem;
}
}
} }
.database-name { .misc-name {
line-height: 1;
padding: 0.1rem 1rem 0.1rem 0.1rem;
position: relative;
}
.database-name,
.misc-name {
&:hover { &:hover {
color: $body-font-color; color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05); background: rgba($color: #fff, $alpha: 0.05);
@@ -138,6 +299,18 @@ export default {
margin-left: 1.2rem; margin-left: 1.2rem;
} }
.database-misc {
margin-left: 1.6rem;
.accordion[open] .accordion-header > .misc-icon:first-child::before {
content: "\F0770";
}
.accordion-body {
margin-bottom: 0.2rem;
}
}
.table-size { .table-size {
position: absolute; position: absolute;
right: 0; right: 0;

View File

@@ -10,6 +10,21 @@
<div class="context-element" @click="showCreateTableModal"> <div class="context-element" @click="showCreateTableModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div> </div>
<div class="context-element" @click="showCreateViewModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
</div>
<div class="context-element" @click="showCreateTriggerModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div>
<div class="context-element" @click="showCreateRoutineModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div>
<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" @click="showCreateSchedulerModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div>
</div> </div>
</div> </div>
<div class="context-element" @click="showEditModal"> <div class="context-element" @click="showEditModal">
@@ -84,6 +99,21 @@ export default {
showCreateTableModal () { showCreateTableModal () {
this.$emit('show-create-table-modal'); this.$emit('show-create-table-modal');
}, },
showCreateViewModal () {
this.$emit('show-create-view-modal');
},
showCreateTriggerModal () {
this.$emit('show-create-trigger-modal');
},
showCreateRoutineModal () {
this.$emit('show-create-routine-modal');
},
showCreateFunctionModal () {
this.$emit('show-create-function-modal');
},
showCreateSchedulerModal () {
this.$emit('show-create-scheduler-modal');
},
showDeleteModal () { showDeleteModal () {
this.isDeleteModal = true; this.isDeleteModal = true;
}, },
@@ -124,3 +154,8 @@ export default {
} }
}; };
</script> </script>
<style lang="scss" scoped>
.context-submenu {
min-width: 150px !important;
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<div
v-if="['procedure', 'function'].includes(selectedMisc.type)"
class="context-element disabled"
@click="showRunModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
</div>
<div class="context-element" @click="showDeleteModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div>
<ConfirmModal
v-if="isDeleteModal"
@confirm="deleteMisc"
@hide="hideDeleteModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ deleteMessage }}
</div>
</template>
<div slot="body">
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedMisc.name }}</b>"?
</div>
</div>
</ConfirmModal>
</BaseContextMenu>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
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
},
props: {
contextEvent: MouseEvent,
selectedMisc: Object
},
data () {
return {
isDeleteModal: false,
isRunModal: false
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
deleteMessage () {
switch (this.selectedMisc.type) {
case 'trigger':
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 '';
}
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},
hideDeleteModal () {
this.isDeleteModal = false;
},
showRunModal () {
this.isRunModal = true;
},
hideRunModal () {
this.isRunModal = false;
},
closeContext () {
this.$emit('close-context');
},
async deleteMisc () {
try {
let res;
switch (this.selectedMisc.type) {
case 'trigger':
res = await Triggers.dropTrigger({
uid: this.selectedWorkspace,
trigger: this.selectedMisc.name
});
break;
case 'procedure':
res = await Routines.dropRoutine({
uid: this.selectedWorkspace,
routine: 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;
if (status === 'success') {
this.changeBreadcrumbs({ [this.selectedMisc.type]: null });
this.closeContext();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
}
};
</script>

View File

@@ -3,7 +3,11 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div class="context-element" @click="showEmptyModal"> <div
v-if="selectedTable.type === 'table'"
class="context-element"
@click="showEmptyModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-off text-light pr-1" /> {{ $t('message.emptyTable') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table-off text-light pr-1" /> {{ $t('message.emptyTable') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div class="context-element" @click="showDeleteModal">
@@ -22,7 +26,7 @@
</template> </template>
<div slot="body"> <div slot="body">
<div class="mb-2"> <div class="mb-2">
{{ $t('message.emptyCorfirm') }} "<b>{{ selectedTable }}</b>"? {{ $t('message.emptyCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
</div> </div>
</div> </div>
</ConfirmModal> </ConfirmModal>
@@ -33,12 +37,12 @@
> >
<template slot="header"> <template slot="header">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-table-remove mr-1" /> {{ $t('message.deleteTable') }} <i class="mdi mdi-24px mdi-table-remove mr-1" /> {{ selectedTable.type === 'table' ? $t('message.deleteTable') : $t('message.deleteView') }}
</div> </div>
</template> </template>
<div slot="body"> <div slot="body">
<div class="mb-2"> <div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedTable }}</b>"? {{ $t('message.deleteCorfirm') }} "<b>{{ selectedTable.name }}</b>"?
</div> </div>
</div> </div>
</ConfirmModal> </ConfirmModal>
@@ -50,6 +54,7 @@ import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
export default { export default {
name: 'WorkspaceExploreBarTableContext', name: 'WorkspaceExploreBarTableContext',
@@ -59,7 +64,7 @@ export default {
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
selectedTable: String selectedTable: Object
}, },
data () { data () {
return { return {
@@ -103,11 +108,11 @@ export default {
try { try {
const { status, response } = await Tables.truncateTable({ const { status, response } = await Tables.truncateTable({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
table: this.selectedTable table: this.selectedTable.name
}); });
if (status === 'success') { if (status === 'success') {
if (this.selectedTable === this.workspace.breadcrumbs.table) if (this.selectedTable.name === this.workspace.breadcrumbs.table)
this.changeBreadcrumbs({ table: null }); this.changeBreadcrumbs({ table: null });
this.closeContext(); this.closeContext();
@@ -122,14 +127,26 @@ export default {
}, },
async deleteTable () { async deleteTable () {
try { try {
const { status, response } = await Tables.dropTable({ let res;
uid: this.selectedWorkspace,
table: this.selectedTable if (this.selectedTable.type === 'table') {
}); res = await Tables.dropTable({
uid: this.selectedWorkspace,
table: this.selectedTable.name
});
}
else if (this.selectedTable.type === 'view') {
res = await Views.dropView({
uid: this.selectedWorkspace,
view: this.selectedTable.name
});
}
const { status, response } = res;
if (status === 'success') { if (status === 'success') {
if (this.selectedTable === this.workspace.breadcrumbs.table) if (this.selectedTable.name === this.workspace.breadcrumbs.table || this.selectedTable.name === this.workspace.breadcrumbs.view)
this.changeBreadcrumbs({ table: null }); this.changeBreadcrumbs({ table: null, view: null });
this.closeContext(); this.closeContext();
this.$emit('reload'); this.$emit('reload');

View File

@@ -0,0 +1,416 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmForeignsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-key-link mr-1" /> {{ $t('word.foreignKeys') }} "{{ table }}"
</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="addForeign">
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-link-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="indexesPanel" class="panel-body p-0 pr-1">
<div
v-for="foreign in foreignProxy"
:key="foreign._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-foreign': selectedForeignID === foreign._id}"
@click="selectForeign($event, foreign._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-key-link mdi-24px" />
</div>
</div>
<div class="tile-content">
<div class="tile-title">
{{ foreign.constraintName }}
</div>
<small class="tile-subtitle text-gray d-flex">
<i class="mdi mdi-link-variant mr-1" />
<div class="fk-details-wrapper">
<span v-if="foreign.table !== ''" class="fk-details">
<i class="mdi mdi-table mr-1" />
<span>{{ foreign.table }}.{{ foreign.field }}</span>
</span>
<span v-if="foreign.refTable !== ''" class="fk-details">
<i class="mdi mdi-table mr-1" />
<span>{{ foreign.refTable }}.{{ foreign.refField }}</span>
</span>
</div>
</small>
</div>
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeIndex(foreign._id)"
>
<i class="mdi mdi-close" />
</button>
</div>
</div>
</div>
</div>
</div>
<div class="column col-7 pl-2 editor-col">
<form
v-if="selectedForeignObj"
: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="selectedForeignObj.constraintName"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group mb-4">
<label class="form-label col-3">
{{ $tc('word.field', 1) }}
</label>
<div class="fields-list column pt-1">
<label
v-for="(field, i) in fields"
:key="`${field.name}-${i}`"
class="form-checkbox m-0"
@click.prevent="toggleField(field.name)"
>
<input type="checkbox" :checked="selectedForeignObj.field === field.name">
<i class="form-icon" /> {{ field.name }}
</label>
</div>
</div>
<div class="form-group">
<label class="form-label col-3 pt-0">
{{ $t('message.referenceTable') }}
</label>
<div class="column">
<select
v-model="selectedForeignObj.refTable"
class="form-select"
@change="reloadRefFields"
>
<option
v-for="schemaTable in schemaTables"
:key="schemaTable.name"
:value="schemaTable.name"
>
{{ schemaTable.name }}
</option>
</select>
</div>
</div>
<div class="form-group mb-4">
<label class="form-label col-3">
{{ $t('message.referenceField') }}
</label>
<div class="fields-list column pt-1">
<label
v-for="(field, i) in refFields[selectedForeignID]"
:key="`${field.name}-${i}`"
class="form-checkbox m-0"
@click.prevent="toggleRefField(field.name)"
>
<input type="checkbox" :checked="selectedForeignObj.refField === field.name && selectedForeignObj.refTable === field.table">
<i class="form-icon" /> {{ field.name }}
</label>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('message.onUpdate') }}
</label>
<div class="column">
<select v-model="selectedForeignObj.onUpdate" class="form-select">
<option
v-for="action in foreignActions"
:key="action"
:value="action"
>
{{ action }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('message.onDelete') }}
</label>
<div class="column">
<select v-model="selectedForeignObj.onDelete" class="form-select">
<option
v-for="action in foreignActions"
:key="action"
:value="action"
>
{{ action }}
</option>
</select>
</div>
</div>
</form>
<div v-if="!foreignProxy.length" class="empty">
<div class="empty-icon">
<i class="mdi mdi-key-link mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoForeign') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addForeign">
{{ $t('message.createNewForeign') }}
</button>
</div>
</div>
</div>
</div>
</div>
</ConfirmModal>
</template>
<script>
import { mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsForeignModal',
components: {
ConfirmModal
},
props: {
localKeyUsage: Array,
connection: Object,
table: String,
schema: String,
schemaTables: Array,
fields: Array,
workspace: Object
},
data () {
return {
foreignProxy: [],
isOptionsChanging: false,
selectedForeignID: '',
modalInnerHeight: 400,
refFields: {},
foreignActions: [
'RESTRICT',
'CASCADE',
'SET NULL',
'NO ACTION'
]
};
},
computed: {
selectedForeignObj () {
return this.foreignProxy.find(foreign => foreign._id === this.selectedForeignID);
},
isChanged () {
return JSON.stringify(this.localKeyUsage) !== JSON.stringify(this.foreignProxy);
},
hasPrimary () {
return this.foreignProxy.some(foreign => foreign.type === 'PRIMARY');
}
},
mounted () {
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage));
if (this.foreignProxy.length)
this.resetSelectedID();
if (this.selectedForeignObj)
this.getRefFields();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
},
destroyed () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
confirmForeignsChange () {
this.$emit('foreigns-update', this.foreignProxy);
},
selectForeign (event, id) {
if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field'))
this.selectedForeignID = id;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addForeign () {
this.foreignProxy = [...this.foreignProxy, {
_id: uidGen(),
constraintName: `FK_${this.foreignProxy.length + 1}`,
refSchema: this.schema,
table: this.table,
refTable: '',
field: '',
refField: '',
onUpdate: this.foreignActions[0],
onDelete: this.foreignActions[0]
}];
if (this.foreignProxy.length === 1)
this.resetSelectedID();
setTimeout(() => {
this.$refs.indexesPanel.scrollTop = this.$refs.indexesPanel.scrollHeight + 60;
}, 20);
},
removeIndex (id) {
this.foreignProxy = this.foreignProxy.filter(foreign => foreign._id !== id);
if (this.selectedForeignID === id && this.foreignProxy.length)
this.resetSelectedID();
},
clearChanges () {
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage));
if (!this.foreignProxy.some(foreign => foreign._id === this.selectedForeignID))
this.resetSelectedID();
},
toggleField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID)
foreign.field = field;
return foreign;
});
},
toggleRefField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID)
foreign.refField = field;
return foreign;
});
},
resetSelectedID () {
this.selectedForeignID = this.foreignProxy.length ? this.foreignProxy[0]._id : '';
},
async getRefFields () {
const params = {
uid: this.connection.uid,
schema: this.selectedForeignObj.refSchema,
table: this.selectedForeignObj.refTable
};
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
this.refFields = {
...this.refFields,
[this.selectedForeignID]: response
};
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
reloadRefFields () {
this.selectedForeignObj.refField = '';
this.getRefFields();
}
}
};
</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-foreign {
background: $bg-color-light;
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 80px;
overflow: auto;
}
.remove-field .mdi {
pointer-events: none;
}
.fk-details-wrapper {
max-width: calc(100% - 1rem);
.fk-details {
display: flex;
line-height: 1;
align-items: baseline;
> span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
padding-bottom: 2px;
}
}
}
</style>

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,270 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmIndexesChange"
@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.name"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}"
@click="selectParameter($event, param.name)"
>
<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.name)"
>
<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 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.name === 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: {
confirmIndexesChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, name) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field'))
this.selectedParam = name;
},
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, {
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 (name) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name);
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].name : '';
}
}
};
</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

@@ -65,37 +65,45 @@
</div> </div>
<div class="column col-7 pl-2 editor-col"> <div class="column col-7 pl-2 editor-col">
<form v-if="selectedIndexObj" :style="{ height: modalInnerHeight + 'px'}"> <form
v-if="selectedIndexObj"
:style="{ height: modalInnerHeight + 'px'}"
class="form-horizontal"
>
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label col-3">
{{ $t('word.name') }} {{ $t('word.name') }}
</label> </label>
<input <div class="column">
v-model="selectedIndexObj.name" <input
class="form-input" v-model="selectedIndexObj.name"
type="text" class="form-input"
> type="text"
>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label col-3">
{{ $t('word.type') }} {{ $t('word.type') }}
</label> </label>
<select v-model="selectedIndexObj.type" class="form-select"> <div class="column">
<option <select v-model="selectedIndexObj.type" class="form-select">
v-for="index in indexTypes" <option
:key="index" v-for="index in indexTypes"
:value="index" :key="index"
:disabled="index === 'PRIMARY' && hasPrimary" :value="index"
> :disabled="index === 'PRIMARY' && hasPrimary"
{{ index }} >
</option> {{ index }}
</select> </option>
</select>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label col-3">
{{ $tc('word.field', fields.length) }} {{ $tc('word.field', fields.length) }}
</label> </label>
<div class="fields-list"> <div class="fields-list column pt-1">
<label <label
v-for="(field, i) in fields" v-for="(field, i) in fields"
:key="`${field.name}-${i}`" :key="`${field.name}-${i}`"
@@ -108,6 +116,19 @@
</div> </div>
</div> </div>
</form> </form>
<div v-if="!indexesProxy.length" class="empty">
<div class="empty-icon">
<i class="mdi mdi-key-outline mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoIndexes') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addIndex">
{{ $t('message.createNewIndex') }}
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -216,7 +237,7 @@ export default {
}); });
}, },
resetSelectedID () { resetSelectedID () {
this.selectedIndexID = this.indexesProxy[0]._id; this.selectedIndexID = this.indexesProxy.length ? this.indexesProxy[0]._id : '';
} }
} }
}; };
@@ -253,7 +274,7 @@ export default {
} }
.fields-list { .fields-list {
max-height: 200px; max-height: 300px;
overflow: auto; overflow: auto;
} }

View File

@@ -18,6 +18,7 @@
</label> </label>
<div class="column"> <div class="column">
<input <input
ref="firstInput"
v-model="optionsProxy.name" v-model="optionsProxy.name"
class="form-input" class="form-input"
:class="{'is-error': !isTableNameValid}" :class="{'is-error': !isTableNameValid}"
@@ -112,6 +113,10 @@ export default {
}, },
created () { created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions)); this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
}, },
methods: { methods: {
confirmOptionsChange () { confirmOptionsChange () {

View File

@@ -0,0 +1,145 @@
<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.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: 'WorkspacePropsRoutineOptionsModal',
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,301 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmIndexesChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> {{ $t('word.parameters') }} "{{ routine }}"
</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.name"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}"
@click="selectParameter($event, param.name)"
>
<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})` : '' }} · {{ param.context }}</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.name)"
>
<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>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
</label>
<div class="column">
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
name="context"
value="IN"
> <i class="form-icon" /> IN
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="OUT"
name="context"
> <i class="form-icon" /> OUT
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="INOUT"
name="context"
> <i class="form-icon" /> INOUT
</label>
</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 ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineParamsModal',
components: {
ConfirmModal
},
props: {
localParameters: Array,
routine: String,
workspace: Object
},
data () {
return {
parametersProxy: [],
isOptionsChanging: false,
selectedParam: '',
modalInnerHeight: 400,
i: 1
};
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param.name === 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: {
confirmIndexesChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, name) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field'))
this.selectedParam = name;
},
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, {
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 (name) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name);
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].name : '';
}
}
};
</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

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

@@ -40,7 +40,7 @@
<span>{{ $t('word.indexes') }}</span> <span>{{ $t('word.indexes') }}</span>
<i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" /> <i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" />
</button> </button>
<button class="btn btn-dark btn-sm d-none"> <button class="btn btn-dark btn-sm" @click="showForeignModal">
<span>{{ $t('word.foreignKeys') }}</span> <span>{{ $t('word.foreignKeys') }}</span>
<i class="mdi mdi-24px mdi-key-link ml-1" /> <i class="mdi mdi-24px mdi-key-link ml-1" />
</button> </button>
@@ -51,12 +51,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="workspace-query-results column col-12"> <div class="workspace-query-results column col-12 p-relative">
<BaseLoader v-if="isLoading" />
<WorkspacePropsTable <WorkspacePropsTable
v-if="localFields" v-if="localFields"
ref="indexTable" ref="indexTable"
:fields="localFields" :fields="localFields"
:indexes="localIndexes" :indexes="localIndexes"
:foreigns="localKeyUsage"
:tab-uid="tabUid" :tab-uid="tabUid"
:conn-uid="connection.uid" :conn-uid="connection.uid"
:index-types="workspace.indexTypes" :index-types="workspace.indexTypes"
@@ -86,6 +88,18 @@
@hide="hideIndexesModal" @hide="hideIndexesModal"
@indexes-update="indexesUpdate" @indexes-update="indexesUpdate"
/> />
<WorkspacePropsForeignModal
v-if="isForeignModal"
:local-key-usage="localKeyUsage"
:connection="connection"
:table="table"
:schema="schema"
:schema-tables="schemaTables"
:fields="localFields"
:workspace="workspace"
@hide="hideForeignModal"
@foreigns-update="foreignsUpdate"
/>
</div> </div>
</template> </template>
@@ -93,16 +107,20 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsTable from '@/components/WorkspacePropsTable'; import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal'; import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal'; import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
import WorkspacePropsForeignModal from '@/components/WorkspacePropsForeignModal';
export default { export default {
name: 'WorkspacePropsTab', name: 'WorkspacePropsTab',
components: { components: {
BaseLoader,
WorkspacePropsTable, WorkspacePropsTable,
WorkspacePropsOptionsModal, WorkspacePropsOptionsModal,
WorkspacePropsIndexesModal WorkspacePropsIndexesModal,
WorkspacePropsForeignModal
}, },
props: { props: {
connection: Object, connection: Object,
@@ -111,10 +129,11 @@ export default {
data () { data () {
return { return {
tabUid: 'prop', tabUid: 'prop',
isQuering: false, isLoading: false,
isSaving: false, isSaving: false,
isOptionsModal: false, isOptionsModal: false,
isIndexesModal: false, isIndexesModal: false,
isForeignModal: false,
isOptionsChanging: false, isOptionsChanging: false,
originalFields: [], originalFields: [],
localFields: [], localFields: [],
@@ -148,6 +167,13 @@ export default {
schema () { schema () {
return this.workspace.breadcrumbs.schema; return this.workspace.breadcrumbs.schema;
}, },
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') : [];
},
isChanged () { isChanged () {
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) || return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) ||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) || JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
@@ -177,12 +203,15 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure', refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges' setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}), }),
async getFieldsData () { async getFieldsData () {
if (!this.table) return; if (!this.table) return;
this.localFields = [];
this.newFieldsCounter = 0; this.newFieldsCounter = 0;
this.isQuering = true; this.isLoading = true;
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
const params = { const params = {
@@ -242,8 +271,13 @@ export default {
const { status, response } = await Tables.getKeyUsage(params); const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success') { if (status === 'success') {
this.originalKeyUsage = response; this.originalKeyUsage = response.map(foreign => {
this.localKeyUsage = JSON.parse(JSON.stringify(response)); return {
_id: uidGen(),
...foreign
};
});
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
} }
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
@@ -252,7 +286,7 @@ export default {
this.addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
} }
this.isQuering = false; this.isLoading = false;
}, },
async saveChanges () { async saveChanges () {
if (this.isSaving) return; if (this.isSaving) return;
@@ -321,6 +355,35 @@ export default {
// Index Deletions // Index Deletions
indexChanges.deletions = this.originalIndexes.filter(index => !localIndexIDs.includes(index._id)); indexChanges.deletions = this.originalIndexes.filter(index => !localIndexIDs.includes(index._id));
// FOREIGN KEYS
const foreignChanges = {
additions: [],
changes: [],
deletions: []
};
const originalForeignIDs = this.originalKeyUsage.reduce((acc, curr) => [...acc, curr._id], []);
const localForeignIDs = this.localKeyUsage.reduce((acc, curr) => [...acc, curr._id], []);
// Foreigns Additions
foreignChanges.additions = this.localKeyUsage.filter(foreign => !originalForeignIDs.includes(foreign._id));
// Foreigns Changes
this.originalKeyUsage.forEach(originalForeign => {
const lI = this.localKeyUsage.findIndex(localForeign => localForeign._id === originalForeign._id);
if (JSON.stringify(originalForeign) !== JSON.stringify(this.localKeyUsage[lI])) {
if (this.localKeyUsage[lI]) {
foreignChanges.changes.push({
...this.localKeyUsage[lI],
oldName: originalForeign.constraintName
});
}
}
});
// Foreigns Deletions
foreignChanges.deletions = this.originalKeyUsage.filter(foreign => !localForeignIDs.includes(foreign._id));
// ALTER
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
schema: this.schema, schema: this.schema,
@@ -329,14 +392,23 @@ export default {
changes, changes,
deletions, deletions,
indexChanges, indexChanges,
foreignChanges,
options options
}; };
try { // Key usage (foreign keys) try {
const { status, response } = await Tables.alterTable(params); const { status, response } = await Tables.alterTable(params);
if (status === 'success') { if (status === 'success') {
const oldName = this.tableOptions.name;
await this.refreshStructure(this.connection.uid); await this.refreshStructure(this.connection.uid);
if (oldName !== this.localOptions.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, table: this.localOptions.name });
}
this.getFieldsData(); this.getFieldsData();
} }
else else
@@ -423,6 +495,15 @@ export default {
}, },
indexesUpdate (indexes) { indexesUpdate (indexes) {
this.localIndexes = indexes; this.localIndexes = indexes;
},
showForeignModal () {
this.isForeignModal = true;
},
hideForeignModal () {
this.isForeignModal = false;
},
foreignsUpdate (foreigns) {
this.localKeyUsage = foreigns;
} }
} }
}; };

View File

@@ -0,0 +1,267 @@
<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"
:disabled="isChanged"
@click="false"
>
<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"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsFunctionOptionsModal from '@/components/WorkspacePropsFunctionOptionsModal';
import WorkspacePropsFunctionParamsModal from '@/components/WorkspacePropsFunctionParamsModal';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspacePropsTabFunction',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsFunctionOptionsModal,
WorkspacePropsFunctionParamsModal
},
props: {
connection: Object,
function: String
},
data () {
return {
tabUid: 'prop',
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: 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'
}),
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.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 };
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
}
}
};
</script>

View File

@@ -0,0 +1,266 @@
<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"
:disabled="isChanged"
@click="false"
>
<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.routineBody') }}</label>
<QueryEditor
v-if="isSelected"
ref="queryEditor"
:value.sync="localRoutine.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspacePropsRoutineOptionsModal
v-if="isOptionsModal"
:local-options="localRoutine"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsRoutineParamsModal
v-if="isParamsModal"
:local-parameters="localRoutine.parameters"
:workspace="workspace"
:routine="localRoutine.name"
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import Routines from '@/ipc-api/Routines';
export default {
name: 'WorkspacePropsTabRoutine',
components: {
QueryEditor,
BaseLoader,
WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal
},
props: {
connection: Object,
routine: String
},
data () {
return {
tabUid: 'prop',
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
originalRoutine: null,
localRoutine: { sql: '' },
lastRoutine: 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.originalRoutine) !== JSON.stringify(this.localRoutine);
},
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.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 routine () {
if (this.isSelected) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
}
},
async isSelected (val) {
if (val && this.lastRoutine !== this.routine) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
}
},
isChanged (val) {
if (this.isSelected && this.lastRoutine === this.routine && this.routine !== 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 getRoutineData () {
if (!this.routine) return;
this.localRoutine = { sql: '' };
this.isLoading = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
routine: this.workspace.breadcrumbs.procedure
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') {
this.originalRoutine = response;
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.sqlProxy = this.localRoutine.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,
routine: {
...this.localRoutine,
oldName: this.originalRoutine.name
}
};
try {
const { status, response } = await Routines.alterRoutine(params);
if (status === 'success') {
const oldName = this.originalRoutine.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localRoutine.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, procedure: this.localRoutine.name });
}
this.getRoutineData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.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.localRoutine = options;
},
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
}
}
};
</script>

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-3">
<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-3">
<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-6">
<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

@@ -0,0 +1,280 @@
<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>
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
v-model="localTrigger.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localTrigger.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalTrigger.definer">
{{ originalTrigger.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>
<div class="columns">
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label>
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
</div>
</div>
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label>
<div class="input-group">
<select v-model="localTrigger.event1" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select v-model="localTrigger.event2" class="form-select">
<option>INSERT</option>
<option>UPDATE</option>
<option>DELETE</option>
</select>
</div>
</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.triggerStatement') }}</label>
<QueryEditor
v-if="isSelected"
ref="queryEditor"
:value.sync="localTrigger.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script>
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: {
connection: Object,
trigger: String
},
data () {
return {
tabUid: 'prop',
isLoading: false,
isSaving: false,
originalTrigger: null,
localTrigger: { sql: '' },
lastTrigger: 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.originalTrigger) !== JSON.stringify(this.localTrigger);
},
isDefinerInUsers () {
return this.originalTrigger ? this.workspace.users.some(user => this.originalTrigger.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 trigger () {
if (this.isSelected) {
await this.getTriggerData();
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
this.lastTrigger = this.trigger;
}
},
async isSelected (val) {
if (val && this.lastTrigger !== this.trigger) {
await this.getTriggerData();
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
this.lastTrigger = this.trigger;
}
},
isChanged (val) {
if (this.isSelected && this.lastTrigger === this.trigger && this.trigger !== 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 getTriggerData () {
if (!this.trigger) return;
this.localTrigger = { sql: '' };
this.isLoading = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
trigger: this.workspace.breadcrumbs.trigger
};
try {
const { status, response } = await Triggers.getTriggerInformations(params);
if (status === 'success') {
this.originalTrigger = response;
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
this.sqlProxy = this.localTrigger.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,
trigger: {
...this.localTrigger,
oldName: this.originalTrigger.name
}
};
try {
const { status, response } = await Triggers.alterTrigger(params);
if (status === 'success') {
const oldName = this.originalTrigger.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localTrigger.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, trigger: this.localTrigger.name });
}
this.getTriggerData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.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();
}
}
}
};
</script>

View File

@@ -0,0 +1,333 @@
<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>
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-3">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localView.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalView.definer">
{{ originalView.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>
<div class="columns">
<div class="column col-2">
<div class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="DEFINER"
>
<i class="form-icon" /> DEFINER
</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="INVOKER"
>
<i class="form-icon" /> INVOKER
</label>
</div>
</div>
<div class="column col-2">
<div class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
name="algorithm"
value="UNDEFINED"
>
<i class="form-icon" /> UNDEFINED
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="MERGE"
name="algorithm"
>
<i class="form-icon" /> MERGE
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="TEMPTABLE"
name="algorithm"
>
<i class="form-icon" /> TEMPTABLE
</label>
</div>
</div>
<div class="column col-2">
<div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value=""
>
<i class="form-icon" /> None
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="CASCADED"
>
<i class="form-icon" /> CASCADED
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="LOCAL"
>
<i class="form-icon" /> LOCAL
</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.selectStatement') }}</label>
<QueryEditor
v-if="isSelected"
ref="queryEditor"
:value.sync="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<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: {
connection: Object,
view: String
},
data () {
return {
tabUid: 'prop',
isLoading: false,
isSaving: false,
originalView: null,
localView: { sql: '' },
lastView: 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.originalView) !== JSON.stringify(this.localView);
},
isDefinerInUsers () {
return this.originalView ? this.workspace.users.some(user => this.originalView.definer === `\`${user.name}\`@\`${user.host}\``) : true;
}
},
watch: {
async view () {
if (this.isSelected) {
await this.getViewData();
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
this.lastView = this.view;
}
},
async isSelected (val) {
if (val && this.lastView !== this.view) {
await this.getViewData();
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
this.lastView = this.view;
}
},
isChanged (val) {
if (this.isSelected && this.lastView === this.view && this.view !== 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 getViewData () {
if (!this.view) return;
this.isLoading = true;
this.localView = { sql: '' };
const params = {
uid: this.connection.uid,
schema: this.schema,
view: this.workspace.breadcrumbs.view
};
try {
const { status, response } = await Views.getViewInformations(params);
if (status === 'success') {
this.originalView = response;
this.localView = JSON.parse(JSON.stringify(this.originalView));
this.sqlProxy = this.localView.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,
view: {
...this.localView,
oldName: this.originalView.name
}
};
try {
const { status, response } = await Views.alterView(params);
if (status === 'success') {
const oldName = this.originalView.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localView.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, view: this.localView.name });
}
this.getViewData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localView = JSON.parse(JSON.stringify(this.originalView));
this.$refs.queryEditor.editor.session.setValue(this.localView.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();
}
}
}
};
</script>

View File

@@ -29,14 +29,14 @@
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.name') }} {{ $t('word.name') }}
</div> </div>
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.type') }} {{ $t('word.type') }}
</div> </div>
@@ -85,7 +85,7 @@
</div> </div>
</div> </div>
<div class="th"> <div class="th">
<div class="column-resizable"> <div class="column-resizable min-100">
<div class="table-column-title"> <div class="table-column-title">
{{ $t('word.collation') }} {{ $t('word.collation') }}
</div> </div>
@@ -104,6 +104,7 @@
:key="row._id" :key="row._id"
:row="row" :row="row"
:indexes="getIndexes(row.name)" :indexes="getIndexes(row.name)"
:foreigns="getForeigns(row.name)"
:data-types="dataTypes" :data-types="dataTypes"
@contextmenu="contextMenu" @contextmenu="contextMenu"
/> />
@@ -128,6 +129,7 @@ export default {
props: { props: {
fields: Array, fields: Array,
indexes: Array, indexes: Array,
foreigns: Array,
indexTypes: Array, indexTypes: Array,
tabUid: [String, Number], tabUid: [String, Number],
connUid: String, connUid: String,
@@ -214,6 +216,13 @@ export default {
acc.push(...curr.fields.map(f => ({ name: f, type: curr.type }))); acc.push(...curr.fields.map(f => ({ name: f, type: curr.type })));
return acc; return acc;
}, []).filter(f => f.name === field); }, []).filter(f => f.name === field);
},
getForeigns (field) {
return this.foreigns.reduce((acc, curr) => {
if (curr.field === field)
acc.push(`${curr.refTable}.${curr.refField}`);
return acc;
}, []);
} }
} }
}; };
@@ -231,4 +240,8 @@ export default {
.vscroll { .vscroll {
overflow: auto; overflow: auto;
} }
.min-100 {
min-width: 100px !important;
}
</style> </style>

View File

@@ -77,11 +77,3 @@ export default {
} }
}; };
</script> </script>
<style lang="scss" scoped>
.disabled {
pointer-events: none;
filter: grayscale(100%);
opacity: 0.5;
}
</style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)"> <div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)">
<div class="td"> <div class="td" tabindex="0">
<div class="row-draggable"> <div class="row-draggable">
<i class="mdi mdi-drag-horizontal row-draggable-icon" /> <i class="mdi mdi-drag-horizontal row-draggable-icon" />
{{ localRow.order }} {{ localRow.order }}
@@ -15,9 +15,15 @@
class="d-inline-block mdi mdi-key column-key c-help" class="d-inline-block mdi mdi-key column-key c-help"
:class="`key-${index.type}`" :class="`key-${index.type}`"
/> />
<i
v-for="foreign in foreigns"
:key="foreign"
:title="foreign"
class="d-inline-block mdi mdi-key-link c-help"
/>
</div> </div>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<span <span
v-if="!isInlineEditor.name" v-if="!isInlineEditor.name"
class="cell-content" class="cell-content"
@@ -35,11 +41,15 @@
@blur="editOFF" @blur="editOFF"
> >
</div> </div>
<div class="td text-uppercase text-left" :class="`type-${lowerCase(localRow.type)}`"> <div
class="td text-uppercase"
tabindex="0"
>
<span <span
v-if="!isInlineEditor.type" v-if="!isInlineEditor.type"
class="cell-content" class="cell-content text-left"
@dblclick="editON($event, localRow.type.toUpperCase(), 'type')" :class="`type-${lowerCase(localRow.type)}`"
@click="editON($event, localRow.type.toUpperCase(), 'type')"
> >
{{ localRow.type }} {{ localRow.type }}
</span> </span>
@@ -47,7 +57,7 @@
v-else v-else
ref="editField" ref="editField"
v-model="editingContent" v-model="editingContent"
class="editable-field px-1 text-uppercase" class="form-select editable-field small-select text-uppercase"
@blur="editOFF" @blur="editOFF"
> >
<optgroup <optgroup
@@ -66,7 +76,7 @@
</optgroup> </optgroup>
</select> </select>
</div> </div>
<div class="td type-int"> <div class="td type-int" tabindex="0">
<template v-if="fieldType.length"> <template v-if="fieldType.length">
<span <span
v-if="!isInlineEditor.length" v-if="!isInlineEditor.length"
@@ -86,7 +96,7 @@
> >
</template> </template>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.unsigned" v-model="localRow.unsigned"
@@ -96,17 +106,17 @@
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.nullable" v-model="localRow.nullable"
type="checkbox" type="checkbox"
:disabled="localRow.key === 'pri'" :disabled="!isNullable"
> >
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<label class="form-checkbox"> <label class="form-checkbox">
<input <input
v-model="localRow.zerofill" v-model="localRow.zerofill"
@@ -116,12 +126,12 @@
<i class="form-icon" /> <i class="form-icon" />
</label> </label>
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<span class="cell-content" @dblclick="editON($event, localRow.default, 'default')"> <span class="cell-content" @dblclick="editON($event, localRow.default, 'default')">
{{ fieldDefault }} {{ fieldDefault }}
</span> </span>
</div> </div>
<div class="td type-varchar"> <div class="td type-varchar" tabindex="0">
<span <span
v-if="!isInlineEditor.comment" v-if="!isInlineEditor.comment"
class="cell-content" class="cell-content"
@@ -139,12 +149,12 @@
@blur="editOFF" @blur="editOFF"
> >
</div> </div>
<div class="td"> <div class="td" tabindex="0">
<template v-if="fieldType.collation"> <template v-if="fieldType.collation">
<span <span
v-if="!isInlineEditor.collation" v-if="!isInlineEditor.collation"
class="cell-content" class="cell-content"
@dblclick="editON($event, localRow.collation, 'collation')" @click="editON($event, localRow.collation, 'collation')"
> >
{{ localRow.collation }} {{ localRow.collation }}
</span> </span>
@@ -152,7 +162,7 @@
v-else v-else
ref="editField" ref="editField"
v-model="editingContent" v-model="editingContent"
class="editable-field px-1" class="form-select small-select editable-field"
@blur="editOFF" @blur="editOFF"
> >
<option <option
@@ -224,7 +234,7 @@
<label class="form-radio form-inline"> <label class="form-radio form-inline">
<input <input
v-model="defaultValue.type" v-model="defaultValue.type"
:disabled="localRow.key !== 'pri'" :disabled="!canAutoincrement"
type="radio" type="radio"
name="default" name="default"
value="autoincrement" value="autoincrement"
@@ -283,7 +293,8 @@ export default {
props: { props: {
row: Object, row: Object,
dataTypes: Array, dataTypes: Array,
indexes: Array indexes: Array,
foreigns: Array
}, },
data () { data () {
return { return {
@@ -297,6 +308,7 @@ export default {
onUpdate: '' onUpdate: ''
}, },
editingContent: null, editingContent: null,
originalContent: null,
editingField: null editingField: null
}; };
}, },
@@ -325,6 +337,12 @@ export default {
}, },
collations () { collations () {
return this.getWorkspace(this.selectedWorkspace).collations; return this.getWorkspace(this.selectedWorkspace).collations;
},
canAutoincrement () {
return this.indexes.some(index => ['PRIMARY', 'UNIQUE'].includes(index.type));
},
isNullable () {
return !this.indexes.some(index => ['PRIMARY'].includes(index.type));
} }
}, },
watch: { watch: {
@@ -333,6 +351,13 @@ export default {
}, },
row () { row () {
this.localRow = this.row; this.localRow = this.row;
},
indexes () {
if (!this.canAutoincrement)
this.localRow.autoIncrement = false;
if (!this.isNullable)
this.localRow.nullable = false;
} }
}, },
mounted () { mounted () {
@@ -397,6 +422,7 @@ export default {
this.editingField = field; this.editingField = field;
this.editingContent = content; this.editingContent = content;
this.originalContent = content;
const obj = { [field]: true }; const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj }; this.isInlineEditor = { ...this.isInlineEditor, ...obj };
@@ -414,10 +440,10 @@ export default {
editOFF () { editOFF () {
this.localRow[this.editingField] = this.editingContent; this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'type') { if (this.editingField === 'type' && this.editingContent !== this.originalContent) {
this.localRow.numLength = false; this.localRow.numLength = null;
this.localRow.charLength = false; this.localRow.charLength = null;
this.localRow.datePrecision = false; this.localRow.datePrecision = null;
if (this.fieldType.length) { if (this.fieldType.length) {
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.localRow.numLength = 11; if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.localRow.numLength = 11;
@@ -427,6 +453,12 @@ export default {
if (!this.fieldType.collation) if (!this.fieldType.collation)
this.localRow.collation = null; this.localRow.collation = null;
if (!this.fieldType.unsigned)
this.localRow.unsigned = false;
if (!this.fieldType.zerofill)
this.localRow.zerofill = false;
} }
if (this.editingField === 'default') { if (this.editingField === 'default') {
@@ -460,6 +492,7 @@ export default {
}); });
this.editingContent = null; this.editingContent = null;
this.originalContent = null;
this.editingField = null; this.editingField = null;
}, },
hideDefaultModal () { hideDefaultModal () {

View File

@@ -1,7 +1,13 @@
<template> <template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless"> <div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12"> <div class="workspace-query-runner column col-12">
<QueryEditor v-if="isSelected" :value.sync="query" /> <QueryEditor
v-if="isSelected"
:auto-focus="true"
:value.sync="query"
:workspace="workspace"
:schema="schema"
/>
<div class="workspace-query-runner-footer"> <div class="workspace-query-runner-footer">
<div class="workspace-query-buttons"> <div class="workspace-query-buttons">
<button <button
@@ -99,6 +105,7 @@ export default {
if (!query || this.isQuering) return; if (!query || this.isQuering) return;
this.isQuering = true; this.isQuering = true;
this.clearTabData(); this.clearTabData();
this.$refs.queryTable.resetSort();
try { // Query Data try { // Query Data
const params = { const params = {

View File

@@ -80,6 +80,7 @@
<script> <script>
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import arrayToFile from '../libs/arrayToFile';
import { LONG_TEXT, BLOB } from 'common/fieldTypes'; import { LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow'; import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
@@ -123,8 +124,11 @@ export default {
primaryField () { primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false; return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
}, },
isHardSort () {
return this.mode === 'table' && this.localResults.length === 1000;
},
sortedResults () { sortedResults () {
if (this.currentSort) { if (this.currentSort && !this.isHardSort) {
return [...this.localResults].sort((a, b) => { return [...this.localResults].sort((a, b) => {
let modifier = 1; let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort]; const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
@@ -192,7 +196,7 @@ export default {
}, },
fieldLength (field) { fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null; if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numLength || field.datePrecision || field.charLength || 0; return field.length;
}, },
keyName (key) { keyName (key) {
switch (key) { switch (key) {
@@ -228,7 +232,6 @@ export default {
return row[primaryFieldName]; return row[primaryFieldName];
}, },
setLocalResults () { setLocalResults () {
this.resetSort();
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows
? this.resultsWithRows[this.resultsetIndex].rows.map(item => { ? this.resultsWithRows[this.resultsetIndex].rows.map(item => {
return { ...item, _id: uidGen() }; return { ...item, _id: uidGen() };
@@ -342,6 +345,9 @@ export default {
this.currentSortDir = 'asc'; this.currentSortDir = 'asc';
this.currentSort = field; this.currentSort = field;
} }
if (this.isHardSort)
this.$emit('hard-sort', { field: this.currentSort, dir: this.currentSortDir });
}, },
resetSort () { resetSort () {
this.currentSort = ''; this.currentSort = '';
@@ -349,6 +355,20 @@ export default {
}, },
selectResultset (index) { selectResultset (index) {
this.resultsetIndex = index; this.resultsetIndex = index;
},
downloadTable (format, filename) {
if (!this.sortedResults) return;
const rows = [...this.sortedResults].map(row => {
delete row._id;
return row;
});
arrayToFile({
type: format,
content: rows,
filename
});
} }
} }
}; };

View File

@@ -20,6 +20,7 @@
class="editable-field" class="editable-field"
:value.sync="editingContent" :value.sync="editingContent"
:key-usage="getKeyUsage(cKey)" :key-usage="getKeyUsage(cKey)"
size="small"
@blur="editOFF" @blur="editOFF"
/> />
<template v-else> <template v-else>
@@ -157,6 +158,8 @@ export default {
typeFormat (val, type, precision) { typeFormat (val, type, precision) {
if (!val) return val; if (!val) return val;
type = type.toUpperCase();
if (DATE.includes(type)) if (DATE.includes(type))
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val; return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
@@ -254,7 +257,7 @@ export default {
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext); return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
}, },
foreignKeys () { foreignKeys () {
return this.keyUsage.map(key => key.column); return this.keyUsage.map(key => key.field);
}, },
isEditable () { isEditable () {
return this.fields ? !!(this.fields[0].schema && this.fields[0].table) : false; return this.fields ? !!(this.fields[0].schema && this.fields[0].table) : false;
@@ -274,7 +277,7 @@ export default {
if (field) if (field)
type = field.type; type = field.type;
return type; return type.toLowerCase();
}, },
getFieldPrecision (cKey) { getFieldPrecision (cKey) {
let length = 0; let length = 0;
@@ -313,7 +316,7 @@ export default {
editON (event, content, field) { editON (event, content, field) {
if (!this.isEditable) return; if (!this.isEditable) return;
const type = this.getFieldType(field); const type = this.getFieldType(field).toUpperCase(); ;
this.originalContent = content; this.originalContent = content;
this.editingType = type; this.editingType = type;
this.editingField = field; this.editingField = field;
@@ -420,7 +423,7 @@ export default {
this.$emit('select-row', event, row); this.$emit('select-row', event, row);
}, },
getKeyUsage (keyName) { getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName); return this.keyUsage.find(key => key.field === keyName);
} }
} }
}; };

View File

@@ -32,13 +32,30 @@
</div> </div>
</div> </div>
</div> </div>
<button
class="btn btn-dark btn-sm" <button class="btn btn-dark btn-sm" @click="showAddModal">
@click="showAddModal"
>
<span>{{ $t('word.add') }}</span> <span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-playlist-plus ml-1" /> <i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button> </button>
<div class="dropdown export-dropdown">
<button
:disabled="isQuering"
class="btn btn-dark btn-sm dropdown-toggle mr-0 pr-0"
tabindex="0"
>
<span>{{ $t('word.export') }}</span>
<i class="mdi mdi-24px mdi-file-export ml-1" />
<i class="mdi mdi-24px mdi-menu-down" />
</button>
<ul class="menu text-left">
<li class="menu-item">
<a class="c-hand" @click="downloadTable('json')">JSON</a>
</li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li>
</ul>
</div>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div v-if="results.length && results[0].rows"> <div v-if="results.length && results[0].rows">
@@ -63,6 +80,7 @@
mode="table" mode="table"
@update-field="updateField" @update-field="updateField"
@delete-selected="deleteSelected" @delete-selected="deleteSelected"
@hard-sort="hardSort"
/> />
</div> </div>
<ModalNewTableRow <ModalNewTableRow
@@ -102,7 +120,8 @@ export default {
lastTable: null, lastTable: null,
isAddModal: false, isAddModal: false,
autorefreshTimer: 0, autorefreshTimer: 0,
refreshInterval: null refreshInterval: null,
sortParams: {}
}; };
}, },
computed: { computed: {
@@ -133,8 +152,10 @@ export default {
watch: { watch: {
table () { table () {
if (this.isSelected) { if (this.isSelected) {
this.sortParams = {};
this.getTableData(); this.getTableData();
this.lastTable = this.table; this.lastTable = this.table;
this.$refs.queryTable.resetSort();
} }
}, },
isSelected (val) { isSelected (val) {
@@ -156,7 +177,7 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
async getTableData () { async getTableData (sortParams) {
if (!this.table) return; if (!this.table) return;
this.isQuering = true; this.isQuering = true;
@@ -167,7 +188,8 @@ export default {
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
schema: this.schema, schema: this.schema,
table: this.workspace.breadcrumbs.table table: this.workspace.breadcrumbs.table || this.workspace.breadcrumbs.view,
sortParams
}; };
try { // Table data try { // Table data
@@ -188,7 +210,11 @@ export default {
return this.table; return this.table;
}, },
reloadTable () { reloadTable () {
this.getTableData(); this.getTableData(this.sortParams);
},
hardSort (sortParams) {
this.sortParams = sortParams;
this.getTableData(sortParams);
}, },
showAddModal () { showAddModal () {
this.isAddModal = true; this.isAddModal = true;
@@ -213,7 +239,21 @@ export default {
this.reloadTable(); this.reloadTable();
}, this.autorefreshTimer * 1000); }, this.autorefreshTimer * 1000);
} }
},
downloadTable (format) {
this.$refs.queryTable.downloadTable(format, this.table);
} }
} }
}; };
</script> </script>
<style lang="scss" scoped>
.export-dropdown {
.menu {
min-width: 100%;
.menu-item a:hover {
background: $bg-color-gray;
}
}
}
</style>

View File

@@ -61,7 +61,31 @@ module.exports = {
total: 'Total', total: 'Total',
table: 'Table', table: 'Table',
discard: 'Discard', discard: 'Discard',
stay: 'Stay' stay: 'Stay',
author: 'Author',
light: 'Light',
dark: 'Dark',
autoCompletion: 'Auto Completion',
application: 'Application',
editor: 'Editor',
view: 'View',
definer: 'Definer',
algorithm: 'Algorithm',
trigger: 'Trigger | Triggers',
storedRoutine: 'Stored routine | Stored routines',
scheduler: 'Scheduler | Schedulers',
event: 'Event',
parameters: 'Parameters',
function: 'Function | Functions',
deterministic: 'Deterministic',
context: 'Context',
export: 'Export',
returns: 'Returns',
timing: 'Timing',
state: 'State',
execution: 'Execution',
starts: 'Starts',
ends: 'Ends'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@@ -115,7 +139,40 @@ module.exports = {
deleteTable: 'Delete table', deleteTable: 'Delete table',
emptyCorfirm: 'Do you confirm to empty', emptyCorfirm: 'Do you confirm to empty',
unsavedChanges: 'Unsaved changes', unsavedChanges: 'Unsaved changes',
discardUnsavedChanges: 'You have some unsaved changes. By leaving this tab these changes will be discarded.' discardUnsavedChanges: 'You have some unsaved changes. By leaving this tab these changes will be discarded.',
thereAreNoIndexes: 'There are no indexes',
thereAreNoForeign: 'There are no foreign keys',
createNewForeign: 'Create new foreign key',
referenceTable: 'Ref. table',
referenceField: 'Ref. field',
foreignFields: 'Foreign fields',
invalidDefault: 'Invalid default',
onDelete: 'On delete',
applicationTheme: 'Application Theme',
editorTheme: 'Editor Theme',
wrapLongLines: 'Wrap long lines',
selectStatement: 'Select statement',
triggerStatement: 'Trigger statement',
sqlSecurity: 'SQL security',
updateOption: 'Update option',
deleteView: 'Delete view',
createNewView: 'Create new view',
deleteTrigger: 'Delete trigger',
createNewTrigger: 'Create new trigger',
currentUser: 'Current user',
routineBody: 'Routine body',
dataAccess: 'Data access',
thereAreNoParameters: 'There are no parameters',
createNewParameter: 'Create new parameter',
createNewRoutine: 'Create new 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'
}, },
// Date and Time // Date and Time
short: { short: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,8 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getKey (params) {
return ipcRenderer.sendSync('get-key', params);
}
}

View File

@@ -18,8 +18,8 @@ export default class {
return ipcRenderer.invoke('delete-database', params); return ipcRenderer.invoke('delete-database', params);
} }
static getStructure (uid) { static getStructure (params) {
return ipcRenderer.invoke('get-structure', uid); return ipcRenderer.invoke('get-structure', params);
} }
static getCollations (uid) { static getCollations (uid) {

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 getRoutineInformations (params) {
return ipcRenderer.invoke('get-routine-informations', params);
}
static dropRoutine (params) {
return ipcRenderer.invoke('drop-routine', params);
}
static alterRoutine (params) {
return ipcRenderer.invoke('alter-routine', params);
}
static createRoutine (params) {
return ipcRenderer.invoke('create-routine', params);
}
}

View File

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

View File

@@ -0,0 +1,20 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getTriggerInformations (params) {
return ipcRenderer.invoke('get-trigger-informations', params);
}
static dropTrigger (params) {
return ipcRenderer.invoke('drop-trigger', params);
}
static alterTrigger (params) {
return ipcRenderer.invoke('alter-trigger', params);
}
static createTrigger (params) {
return ipcRenderer.invoke('create-trigger', params);
}
}

View File

@@ -0,0 +1,8 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getUsers (params) {
return ipcRenderer.invoke('get-users', params);
}
}

View File

@@ -0,0 +1,20 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getViewInformations (params) {
return ipcRenderer.invoke('get-view-informations', params);
}
static dropView (params) {
return ipcRenderer.invoke('drop-view', params);
}
static alterView (params) {
return ipcRenderer.invoke('alter-view', params);
}
static createView (params) {
return ipcRenderer.invoke('create-view', 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;

File diff suppressed because it is too large Load Diff

View File

@@ -180,6 +180,12 @@ body {
.form-select { .form-select {
cursor: pointer; cursor: pointer;
&.small-select {
height: 1rem;
font-size: 0.7rem;
padding: 1px 0.4rem 0;
}
} }
.form-select, .form-select,
@@ -206,6 +212,7 @@ body {
.input-group .input-group-addon { .input-group .input-group-addon {
border-color: #3f3f3f; border-color: #3f3f3f;
z-index: 1;
} }
.menu { .menu {
@@ -219,7 +226,7 @@ body {
} }
.accordion-body { .accordion-body {
max-height: 500rem !important; max-height: 5000rem !important;
} }
.btn.loading { .btn.loading {
@@ -228,3 +235,7 @@ body {
visibility: hidden; visibility: hidden;
} }
} }
.empty {
color: $body-font-color;
}

View File

@@ -2,7 +2,6 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VuexPersist from 'vuex-persist';
import application from './modules/application.store'; import application from './modules/application.store';
import settings from './modules/settings.store'; import settings from './modules/settings.store';
@@ -12,15 +11,6 @@ import notifications from './modules/notifications.store';
import ipcUpdates from './plugins/ipcUpdates'; 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); Vue.use(Vuex);
export default new Vuex.Store({ export default new Vuex.Store({
@@ -33,7 +23,6 @@ export default new Vuex.Store({
notifications notifications
}, },
plugins: [ plugins: [
vuexLocalStorage.plugin,
ipcUpdates ipcUpdates
] ]
}); });

View File

@@ -1,10 +1,18 @@
'use strict'; 'use strict';
import Store from 'electron-store';
import Application from '../../ipc-api/Application';
const key = Application.getKey();
const persistentStore = new Store({
name: 'connections',
encryptionKey: key
});
export default { export default {
namespaced: true, namespaced: true,
strict: true, strict: true,
state: { state: {
connections: [] connections: persistentStore.get('connections') || []
}, },
getters: { getters: {
getConnections: state => state.connections, getConnections: state => state.connections,
@@ -20,9 +28,11 @@ export default {
mutations: { mutations: {
ADD_CONNECTION (state, connection) { ADD_CONNECTION (state, connection) {
state.connections.push(connection); state.connections.push(connection);
persistentStore.set('connections', state.connections);
}, },
DELETE_CONNECTION (state, connection) { DELETE_CONNECTION (state, connection) {
state.connections = state.connections.filter(el => el.uid !== connection.uid); state.connections = state.connections.filter(el => el.uid !== connection.uid);
persistentStore.set('connections', state.connections);
}, },
EDIT_CONNECTION (state, connection) { EDIT_CONNECTION (state, connection) {
const editedConnections = state.connections.map(conn => { const editedConnections = state.connections.map(conn => {
@@ -31,9 +41,11 @@ export default {
}); });
state.connections = editedConnections; state.connections = editedConnections;
state.selected_conection = {}; state.selected_conection = {};
persistentStore.set('connections', state.connections);
}, },
UPDATE_CONNECTIONS (state, connections) { UPDATE_CONNECTIONS (state, connections) {
state.connections = connections; state.connections = connections;
persistentStore.set('connections', state.connections);
} }
}, },
actions: { actions: {

View File

@@ -1,29 +1,53 @@
'use strict'; 'use strict';
import i18n from '@/i18n'; import i18n from '@/i18n';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
export default { export default {
namespaced: true, namespaced: true,
strict: true, strict: true,
state: { state: {
locale: 'en-US', locale: persistentStore.get('locale') || 'en-US',
explorebar_size: null, explorebar_size: persistentStore.get('explorebar_size') || null,
notifications_timeout: 5 notifications_timeout: persistentStore.get('notifications_timeout') || 5,
auto_complete: persistentStore.get('auto_complete') || true,
line_wrap: persistentStore.get('line_wrap') || true,
application_theme: persistentStore.get('application_theme') || 'dark',
editor_theme: persistentStore.get('editor_theme') || 'twilight'
}, },
getters: { getters: {
getLocale: state => state.locale, getLocale: state => state.locale,
getExplorebarSize: state => state.explorebar_size, getExplorebarSize: state => state.explorebar_size,
getNotificationsTimeout: state => state.notifications_timeout getNotificationsTimeout: state => state.notifications_timeout,
getAutoComplete: state => state.auto_complete,
getLineWrap: state => state.line_wrap,
getApplicationTheme: state => state.application_theme,
getEditorTheme: state => state.editor_theme
}, },
mutations: { mutations: {
SET_LOCALE (state, locale) { SET_LOCALE (state, locale) {
state.locale = locale; state.locale = locale;
i18n.locale = locale; i18n.locale = locale;
persistentStore.set('locale', state.locale);
}, },
SET_NOTIFICATIONS_TIMEOUT (state, timeout) { SET_NOTIFICATIONS_TIMEOUT (state, timeout) {
state.notifications_timeout = timeout; state.notifications_timeout = timeout;
persistentStore.set('notifications_timeout', state.notifications_timeout);
},
SET_AUTO_COMPLETE (state, val) {
state.auto_complete = val;
persistentStore.set('auto_complete', state.auto_complete);
},
SET_LINE_WRAP (state, val) {
state.line_wrap = val;
persistentStore.set('line_wrap', state.line_wrap);
}, },
SET_EXPLOREBAR_SIZE (state, size) { SET_EXPLOREBAR_SIZE (state, size) {
state.explorebar_size = size; state.explorebar_size = size;
persistentStore.set('explorebar_size', state.explorebar_size);
},
SET_EDITOR_THEME (state, theme) {
state.editor_theme = theme;
} }
}, },
actions: { actions: {
@@ -35,6 +59,15 @@ export default {
}, },
changeExplorebarSize ({ commit }, size) { changeExplorebarSize ({ commit }, size) {
commit('SET_EXPLOREBAR_SIZE', size); commit('SET_EXPLOREBAR_SIZE', size);
},
changeAutoComplete ({ commit }, val) {
commit('SET_AUTO_COMPLETE', val);
},
changeLineWrap ({ commit }, val) {
commit('SET_LINE_WRAP', val);
},
changeEditorTheme ({ commit }, theme) {
commit('SET_EDITOR_THEME', theme);
} }
} }
}; };

View File

@@ -1,6 +1,7 @@
'use strict'; 'use strict';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import Database from '@/ipc-api/Database'; import Database from '@/ipc-api/Database';
import Users from '@/ipc-api/Users';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
const tabIndex = []; const tabIndex = [];
let lastBreadcrumbs = {}; let lastBreadcrumbs = {};
@@ -39,6 +40,9 @@ export default {
.filter(workspace => workspace.connected) .filter(workspace => workspace.connected)
.map(workspace => workspace.uid); .map(workspace => workspace.uid);
}, },
getLoadedSchemas: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).loaded_schemas;
},
isUnsavedDiscardModal: state => { isUnsavedDiscardModal: state => {
return state.is_unsaved_discard_modal; return state.is_unsaved_discard_modal;
} }
@@ -64,6 +68,8 @@ export default {
? { ? {
...workspace, ...workspace,
structure: {}, structure: {},
breadcrumbs: {},
loaded_schemas: new Set(),
connected: false connected: false
} }
: workspace); : workspace);
@@ -76,6 +82,19 @@ export default {
} }
: workspace); : 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 }) { REFRESH_COLLATIONS (state, { uid, collations }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? { ? {
@@ -100,6 +119,14 @@ export default {
} }
: workspace); : workspace);
}, },
REFRESH_USERS (state, { uid, users }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
users
}
: workspace);
},
ADD_WORKSPACE (state, workspace) { ADD_WORKSPACE (state, workspace) {
state.workspaces.push(workspace); state.workspaces.push(workspace);
}, },
@@ -111,11 +138,10 @@ export default {
} }
: workspace); : workspace);
}, },
NEW_TAB (state, uid) { NEW_TAB (state, { uid, tab }) {
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1; tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
const newTab = { const newTab = {
uid: uidGen('T'), uid: tab,
index: tabIndex[uid], index: tabIndex[uid],
selected: false, selected: false,
type: 'query', type: 'query',
@@ -190,6 +216,13 @@ export default {
}, },
SET_PENDING_BREADCRUMBS (state, payload) { SET_PENDING_BREADCRUMBS (state, payload) {
state.pending_breadcrumbs = 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: { actions: {
@@ -222,15 +255,17 @@ export default {
dispatch('refreshCollations', connection.uid); dispatch('refreshCollations', connection.uid);
dispatch('refreshVariables', connection.uid); dispatch('refreshVariables', connection.uid);
dispatch('refreshEngines', connection.uid); dispatch('refreshEngines', connection.uid);
dispatch('refreshUsers', connection.uid);
} }
} }
catch (err) { catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true }); dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
} }
}, },
async refreshStructure ({ dispatch, commit }, uid) { async refreshStructure ({ dispatch, commit, getters }, uid) {
try { try {
const { status, response } = await Database.getStructure(uid); const { status, response } = await Database.getStructure({ uid, schemas: getters.getLoadedSchemas(uid) });
if (status === 'error') if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true }); dispatch('notifications/addNotification', { status, message: response }, { root: true });
else else
@@ -240,6 +275,18 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true }); 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) { async refreshCollations ({ dispatch, commit }, uid) {
try { try {
const { status, response } = await Database.getCollations(uid); const { status, response } = await Database.getCollations(uid);
@@ -276,6 +323,18 @@ export default {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true }); dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
} }
}, },
async refreshUsers ({ dispatch, commit }, uid) {
try {
const { status, response } = await Users.getUsers(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('REFRESH_USERS', { uid, users: response });
}
catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
removeConnected ({ commit }, uid) { removeConnected ({ commit }, uid) {
Connection.disconnect(uid); Connection.disconnect(uid);
commit('REMOVE_CONNECTED', uid); commit('REMOVE_CONNECTED', uid);
@@ -286,22 +345,13 @@ export default {
uid, uid,
connected: false, connected: false,
selected_tab: 0, selected_tab: 0,
tabs: [{ tabs: [],
uid: 'data',
type: 'table',
fields: [],
keyUsage: []
},
{
uid: 'prop',
type: 'table',
fields: [],
keyUsage: []
}],
structure: {}, structure: {},
variables: [], variables: [],
collations: [], collations: [],
breadcrumbs: {} users: [],
breadcrumbs: {},
loaded_schemas: new Set()
}; };
commit('ADD_WORKSPACE', workspace); commit('ADD_WORKSPACE', workspace);
@@ -323,7 +373,9 @@ export default {
table: null, table: null,
trigger: null, trigger: null,
procedure: null, procedure: null,
scheduler: null function: null,
scheduler: null,
view: null
}; };
const hasLastChildren = Object.keys(lastBreadcrumbs).filter(b => b !== 'schema').some(b => lastBreadcrumbs[b]); const hasLastChildren = Object.keys(lastBreadcrumbs).filter(b => b !== 'schema').some(b => lastBreadcrumbs[b]);
@@ -336,9 +388,15 @@ export default {
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...breadcrumbsObj, ...payload } }); commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...breadcrumbsObj, ...payload } });
lastBreadcrumbs = { ...breadcrumbsObj, ...payload }; lastBreadcrumbs = { ...breadcrumbsObj, ...payload };
if (payload.schema)
commit('ADD_LOADED_SCHEMA', { uid: getters.getSelected, schema: payload.schema });
}, },
newTab ({ commit }, uid) { newTab ({ commit }, uid) {
commit('NEW_TAB', uid); const tab = uidGen('T');
commit('NEW_TAB', { uid, tab });
commit('SELECT_TAB', { uid, tab });
}, },
removeTab ({ commit }, payload) { removeTab ({ commit }, payload) {
commit('REMOVE_TAB', payload); commit('REMOVE_TAB', payload);

View File

@@ -1,12 +0,0 @@
import { functions } from '@/suggestions/sql/sql-functions';
import { keywords } from '@/suggestions/sql/sql-keywords';
import { operators } from '@/suggestions/sql/sql-operators';
import { variables } from '@/suggestions/sql/sql-variables';
export const completionItemProvider = (monaco) => {
return {
provideCompletionItems () {
return { suggestions: [...functions(monaco), ...keywords(monaco), ...operators(monaco), ...variables(monaco)] };
}
};
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,142 +0,0 @@
export const operators = (monaco) => {
return [{
label: 'ALL',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'ALL'
},
{
label: 'AND',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'AND'
},
{
label: 'ANY',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'ANY'
},
{
label: 'BETWEEN',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'BETWEEN'
},
{
label: 'EXISTS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'EXISTS'
},
{
label: 'IN',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'IN'
},
{
label: 'LIKE',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'LIKE'
},
{
label: 'NOT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'NOT'
},
{
label: 'OR',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'OR'
},
{
label: 'SOME',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'SOME'
},
{
label: 'EXCEPT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'EXCEPT'
},
{
label: 'INTERSECT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'INTERSECT'
},
{
label: 'UNION',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'UNION'
},
{
label: 'APPLY',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'APPLY'
},
{
label: 'CROSS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'CROSS'
},
{
label: 'FULL',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'FULL'
},
{
label: 'INNER',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'INNER'
},
{
label: 'JOIN',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'JOIN'
},
{
label: 'LEFT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'LEFT'
},
{
label: 'OUTER',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'OUTER'
},
{
label: 'RIGHT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'RIGHT'
},
{
label: 'CONTAINS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'CONTAINS'
},
{
label: 'FREETEXT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'FREETEXT'
},
{
label: 'IS',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'IS'
},
{
label: 'NULL',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'NULL'
},
{
label: 'PIVOT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'PIVOT'
},
{
label: 'UNPIVOT',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'UNPIVOT'
},
{
label: 'MATCHED',
kind: monaco.languages.CompletionItemKind.Operator,
insertText: 'MATCHED'
}];
};

View File

@@ -1,172 +0,0 @@
export const variables = (monaco) => {
return [{
label: '@@DATEFIRST',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@DATEFIRST'
},
{
label: '@@DBTS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@DBTS'
},
{
label: '@@LANGID',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@LANGID'
},
{
label: '@@LANGUAGE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@LANGUAGE'
},
{
label: '@@LOCK_TIMEOUT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@LOCK_TIMEOUT'
},
{
label: '@@MAX_CONNECTIONS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@MAX_CONNECTIONS'
},
{
label: '@@MAX_PRECISION',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@MAX_PRECISION'
},
{
label: '@@NESTLEVEL',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@NESTLEVEL'
},
{
label: '@@OPTIONS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@OPTIONS'
},
{
label: '@@REMSERVER',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@REMSERVER'
},
{
label: '@@SERVERNAME',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@SERVERNAME'
},
{
label: '@@SERVICENAME',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@SERVICENAME'
},
{
label: '@@SPID',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@SPID'
},
{
label: '@@TEXTSIZE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TEXTSIZE'
},
{
label: '@@VERSION',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@VERSION'
},
{
label: '@@CURSOR_ROWS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@CURSOR_ROWS'
},
{
label: '@@FETCH_STATUS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@FETCH_STATUS'
},
{
label: '@@DATEFIRST',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@DATEFIRST'
},
{
label: '@@PROCID',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PROCID'
},
{
label: '@@ERROR',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@ERROR'
},
{
label: '@@IDENTITY',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@IDENTITY'
},
{
label: '@@ROWCOUNT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@ROWCOUNT'
},
{
label: '@@TRANCOUNT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TRANCOUNT'
},
{
label: '@@CONNECTIONS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@CONNECTIONS'
},
{
label: '@@CPU_BUSY',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@CPU_BUSY'
},
{
label: '@@IDLE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@IDLE'
},
{
label: '@@IO_BUSY',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@IO_BUSY'
},
{
label: '@@PACKET_ERRORS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PACKET_ERRORS'
},
{
label: '@@PACK_RECEIVED',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PACK_RECEIVED'
},
{
label: '@@PACK_SENT',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@PACK_SENT'
},
{
label: '@@TIMETICKS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TIMETICKS'
},
{
label: '@@TOTAL_ERRORS',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TOTAL_ERRORS'
},
{
label: '@@TOTAL_READ',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TOTAL_READ'
},
{
label: '@@TOTAL_WRITE',
kind: monaco.languages.CompletionItemKind.Variable,
insertText: '@@TOTAL_WRITE'
}];
};

View File

@@ -1,12 +1,8 @@
const webpack = require('webpack'); const webpack = require('webpack');
const MonacoEditorPlugin = require('monaco-editor-webpack-plugin');
module.exports = { module.exports = {
stats: 'errors-warnings', stats: 'errors-warnings',
plugins: [ plugins: [
new MonacoEditorPlugin({
languages: ['sql']
}),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {
PACKAGE_VERSION: JSON.stringify(require('./package.json').version) PACKAGE_VERSION: JSON.stringify(require('./package.json').version)