mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
Compare commits
74 Commits
Author | SHA1 | Date | |
---|---|---|---|
8cafade8b1 | |||
d13b708377 | |||
206597e5b8 | |||
c5458159d1 | |||
1476e899d1 | |||
797ab70e7c | |||
f81312aeb0 | |||
3ed5ea023e | |||
9291a7a7b4 | |||
5cfdc9b92d | |||
15b08d7ea8 | |||
acebe435ff | |||
5712b80022 | |||
d38583262e | |||
e0e2131981 | |||
7470bddd70 | |||
33d1fa2290 | |||
a4122b4eaa | |||
e6602d1bfa | |||
f8cf90a89e | |||
41505bde65 | |||
8ebc3bce92 | |||
45e9cdc591 | |||
3cbfc0e148 | |||
a47e9e1b1f | |||
e95d29c7c3 | |||
e954f04828 | |||
|
85c800f85b | ||
|
e0482244d7 | ||
27769f204f | |||
dfb24c65f3 | |||
0fe71572a5 | |||
db577bfef0 | |||
0805b96a75 | |||
e49823f5c4 | |||
76351005b4 | |||
3e5770f7de | |||
242ddec744 | |||
07654039b6 | |||
249926b8e0 | |||
ae47a978c1 | |||
|
bc53b0b332 | ||
d4175bcbda | |||
c9ba2e5962 | |||
2e49d86677 | |||
c393f86947 | |||
a0b96aa06c | |||
2dc16e8ea8 | |||
ee183886f6 | |||
cef6f681c8 | |||
ea9b489f5f | |||
1658432fd3 | |||
a8cd17748f | |||
580105e9f3 | |||
0626f6f775 | |||
12f5e479f3 | |||
04804b07c7 | |||
053418ee90 | |||
426628f268 | |||
d4ecaf65e5 | |||
27d114beef | |||
936de04cd3 | |||
b7c779eef6 | |||
d560c384f5 | |||
9ecd88870d | |||
07d1e82325 | |||
ce25cd0a31 | |||
319d9beef1 | |||
|
4272efe73b | ||
ed5cf0a8e4 | |||
c70e5b422c | |||
0bf2c8dc9d | |||
d563cec70d | |||
6ee4ef4b8b |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text eol=lf
|
11
.travis.yml
11
.travis.yml
@@ -22,6 +22,7 @@ jobs:
|
||||
- stage: Test
|
||||
script:
|
||||
- npm test
|
||||
|
||||
- stage: Deploy Linux & Windows
|
||||
if: tag IS present
|
||||
os: linux
|
||||
@@ -30,9 +31,17 @@ jobs:
|
||||
- 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:
|
||||
- rm -rf $HOME/.cache/electron-builder/wine
|
||||
|
||||
- stage: Deploy Mac
|
||||
if: tag IS present
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
script:
|
||||
- npm run build -- -p always
|
||||
- npm run build -- -p always
|
||||
|
||||
# - stage: Deploy ARM Linux
|
||||
# if: tag IS present
|
||||
# os: linux
|
||||
# arch: arm64
|
||||
# script:
|
||||
# - npm run build -- --linux AppImage -p always
|
126
CHANGELOG.md
126
CHANGELOG.md
@@ -2,36 +2,118 @@
|
||||
|
||||
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.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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* approximate totals in table tata tab ([e95d29c](https://github.com/Fabio286/antares/commit/e95d29c7c37e24e7cc14b466f9b539fa667042c2))
|
||||
* create new tables ([e6602d1](https://github.com/Fabio286/antares/commit/e6602d1bfa9ca10c6bb078ee80ddc94fb338763d))
|
||||
* display all keys in properties tab ([27769f2](https://github.com/Fabio286/antares/commit/27769f204f731d20c7ba2f838c02b7c2f28fa0c3))
|
||||
* drop and truncate tables ([a4122b4](https://github.com/Fabio286/antares/commit/a4122b4eaaa5b30d97ba5a93df8c9d21c30bc40b))
|
||||
* index management ([41505bd](https://github.com/Fabio286/antares/commit/41505bde6547c0af3c3413248ad8a0d182838bb1))
|
||||
* tables options edit ([0805b96](https://github.com/Fabio286/antares/commit/0805b96a75e439a7d65e8341ecc86fa938679a9f))
|
||||
* unsaved changes reminder ([33d1fa2](https://github.com/Fabio286/antares/commit/33d1fa22905f477924292135b0dcfefe168ee641))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* index deletion issue ([f8cf90a](https://github.com/Fabio286/antares/commit/f8cf90a89e7367c95e164b7dc669506df392b700))
|
||||
* some problems with properties and data tabs when changing database from sidebar ([0fe7157](https://github.com/Fabio286/antares/commit/0fe71572a5e74c17a5c66237351bb0b02c33e824))
|
||||
* sqlEscaper function wrong quotes conversion ([dfb24c6](https://github.com/Fabio286/antares/commit/dfb24c65f3c395d78d27a2f29e9aa8eeb427cff7))
|
||||
|
||||
### [0.0.9](https://github.com/EStarium/antares/compare/v0.0.8...v0.0.9) (2020-11-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ability to edit table fields ([249926b](https://github.com/EStarium/antares/commit/249926b8e040d62d50244362b7f999f26337b93c))
|
||||
* support to aliased tables ([1658432](https://github.com/EStarium/antares/commit/1658432fd30073ba3bffb39b5c4ca69194ae1330))
|
||||
* table fields addition ([0765403](https://github.com/EStarium/antares/commit/07654039b6a99f3115c378b53d659593e5c81f35))
|
||||
* table fields deletion ([242ddec](https://github.com/EStarium/antares/commit/242ddec744814d15657db1ca88b2d865045ea219))
|
||||
* **ui:** display table properties tab ([2dc16e8](https://github.com/EStarium/antares/commit/2dc16e8ea8d6d9b79288335888e155ff180eebf5))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* duplicate header fields on join result tables ([ea9b489](https://github.com/EStarium/antares/commit/ea9b489f5f45cffa4a7ac87873fff070205e88c4))
|
||||
* F9 key shortcut refresh all query tabs instead of just selected one ([c9ba2e5](https://github.com/EStarium/antares/commit/c9ba2e5962eae1afc46daf35e55fb0ea5c3af5a4))
|
||||
* issue with tabs horizontal scroll with wheel ([c393f86](https://github.com/EStarium/antares/commit/c393f86947d1f65f896cadaa39d53f13e0a1f4eb))
|
||||
* zero fill field option was not saved ([3e5770f](https://github.com/EStarium/antares/commit/3e5770f7de51bdf2bc0f1f38c7ceb9ef0f4dcd00))
|
||||
* **mysql:** error getting foreign key list ([ee18388](https://github.com/EStarium/antares/commit/ee183886f64947305cc4f0d38dbdf7919953ec01))
|
||||
* wrong result fields type and order with some queries ([a8cd177](https://github.com/EStarium/antares/commit/a8cd17748f4ac7d75092f65ae7ca5f96a8a9e8c5))
|
||||
|
||||
### [0.0.8](https://github.com/EStarium/antares/compare/v0.0.7...v0.0.8) (2020-10-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **render:** field type and length on table header mouse hover ([04804b0](https://github.com/EStarium/antares/commit/04804b07c71cec271c31ace13bd41b2c7415e892))
|
||||
* close modals pressing ESC ([d563cec](https://github.com/EStarium/antares/commit/d563cec70d996f66c4f724bba7de618fc8678e66))
|
||||
* data table autorefresh, closes [#36](https://github.com/EStarium/antares/issues/36) ([9ecd888](https://github.com/EStarium/antares/commit/9ecd88870d1fcf32bb2c970a1506206c477810a0))
|
||||
* pie chart with table size in database explore bar ([426628f](https://github.com/EStarium/antares/commit/426628f268c77496a13b3498f03fd7b11fee299a))
|
||||
* query and data tabs keyboard shortcuts (F5, F9) ([0bf2c8d](https://github.com/EStarium/antares/commit/0bf2c8dc9dd9bdf7a8f48bed61eed7f1f1aacf71))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* context menu outside window when near bottom or right edge ([d4ecaf6](https://github.com/EStarium/antares/commit/d4ecaf65e56044170139bac61c3ee69efc35a8f0))
|
||||
* disable cell editor for not editable results ([b7c779e](https://github.com/EStarium/antares/commit/b7c779eef63c257c166e7128ea643bdd6142aa88))
|
||||
* missing connection name when "ask for crendential" selected ([c70e5b4](https://github.com/EStarium/antares/commit/c70e5b422c3534e92a64a0b534eb58663621489c))
|
||||
* missing header for some query results ([d560c38](https://github.com/EStarium/antares/commit/d560c384f5aed58ea975935975843c3b9061dd85))
|
||||
* no connection passed to connection's edit modal ([ce25cd0](https://github.com/EStarium/antares/commit/ce25cd0a3130db486ea4da24dd393d45c2ef9e0d))
|
||||
|
||||
### [0.0.7](https://github.com/EStarium/antares/compare/v0.0.6...v0.0.7) (2020-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* database creation ([3d0a83f](https://github.com/EStarium/antares/commit/3d0a83f2cf68c4dd412fd7679c39d63f081b7c19))
|
||||
* databases deletion ([4288a1f](https://github.com/EStarium/antares/commit/4288a1fd331f4a28de2e756f898d208a6a6599c4))
|
||||
* edit database collation ([54717e1](https://github.com/EStarium/antares/commit/54717e1f6a36ec0b3dd096d0e1e747512f6dda09))
|
||||
* field comment on mouse over a table field name ([2554444](https://github.com/EStarium/antares/commit/2554444322b59a6b1ab3ff05ccf8604bf6f8c8b8))
|
||||
* support to multiple queries in the same tab ([48f77ba](https://github.com/EStarium/antares/commit/48f77bae01efbff40bd0f5ce8c66e2619f44bf3a))
|
||||
* update italian translation ([89c3dc9](https://github.com/EStarium/antares/commit/89c3dc9fede63c77eb22b48df1a375ea44830306))
|
||||
* Update italian translation ([fe3d741](https://github.com/EStarium/antares/commit/fe3d7416013c44a4974471ab59b7c9a98afb7255))
|
||||
|
||||
* Database creation ([3d0a83f](https://github.com/EStarium/antares/commit/3d0a83f2cf68c4dd412fd7679c39d63f081b7c19))
|
||||
* Database deletion ([4288a1f](https://github.com/EStarium/antares/commit/4288a1fd331f4a28de2e756f898d208a6a6599c4))
|
||||
* Edit database collation ([54717e1](https://github.com/EStarium/antares/commit/54717e1f6a36ec0b3dd096d0e1e747512f6dda09))
|
||||
* Field comment on mouse over a table field name ([2554444](https://github.com/EStarium/antares/commit/2554444322b59a6b1ab3ff05ccf8604bf6f8c8b8))
|
||||
* Support to multiple queries in the same tab ([48f77ba](https://github.com/EStarium/antares/commit/48f77bae01efbff40bd0f5ce8c66e2619f44bf3a))
|
||||
* Update italian translation ([89c3dc9](https://github.com/EStarium/antares/commit/89c3dc9fede63c77eb22b48df1a375ea44830306))
|
||||
* **Spanish translation** thanks to
|
||||
[hongkfui](https://github.com/hongkfui) ([#32](https://github.com/EStarium/antares/pull/32))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cell update soft reload doesn't apply changes ([1b04b21](https://github.com/EStarium/antares/commit/1b04b216b21b697e47062a9366bc1b6a040a1a72))
|
||||
* empty databases not shown in explore bar ([3e737cb](https://github.com/EStarium/antares/commit/3e737cba62f795f225e944939c6bff04b27fa3d4))
|
||||
* glitch on table data tab ([10b426b](https://github.com/EStarium/antares/commit/10b426b90b6b9461cfffce3026c982463f6e0599))
|
||||
* lack of loading progressbar when an update is available ([86aec4f](https://github.com/EStarium/antares/commit/86aec4f5e41c059e88066a01f0d85155de99a5ee))
|
||||
* missing schema when queryng INFORMATION_SCHEMA ([530d1bd](https://github.com/EStarium/antares/commit/530d1bd43fa95de05f594b9b5cae2f4b397f96e0))
|
||||
* prevent multiple app instances ([12fbe8c](https://github.com/EStarium/antares/commit/12fbe8c1a03259648554f2a5c69b5abbedc18a48))
|
||||
* several fix on data and query tabs ([530907d](https://github.com/EStarium/antares/commit/530907d097ac4d995e1bfcb02e6c890fd6007e21))
|
||||
* unable to obtain fields informations for some queries ([43c7072](https://github.com/EStarium/antares/commit/43c7072c1c83a2455ae48a37be69b444b3eb6560))
|
||||
* unable to obtain keyUsage informations when adding new row ([023c6a6](https://github.com/EStarium/antares/commit/023c6a633a7f268b1a97b748ad08d2416cc30ffe))
|
||||
* value overridden when join tables with fields with same name ([78965d2](https://github.com/EStarium/antares/commit/78965d23e3efb7d8d6d110d79142966e57200757))
|
||||
* wrong field names when join tables ([ad0bad8](https://github.com/EStarium/antares/commit/ad0bad8486c3d67ec14ec1aed3d8aff6cce9df87))
|
||||
* wrong italian translation ([b29e07c](https://github.com/EStarium/antares/commit/b29e07c3b722aec7e78f3cef2e357a53cbcac474))
|
||||
* wrong schema fetching table fields and key usage ([8e71f42](https://github.com/EStarium/antares/commit/8e71f42a28060fdfeeb81502b0759d0d11f5bcfd))
|
||||
* wrong table and schema when more than one query in a tab ([4684b41](https://github.com/EStarium/antares/commit/4684b4114b9c9c253120292d7d164d7676011f86))
|
||||
* Cell update soft reload doesn't apply changes ([1b04b21](https://github.com/EStarium/antares/commit/1b04b216b21b697e47062a9366bc1b6a040a1a72))
|
||||
* Empty databases not shown in explore bar ([3e737cb](https://github.com/EStarium/antares/commit/3e737cba62f795f225e944939c6bff04b27fa3d4))
|
||||
* Glitch on table data tab ([10b426b](https://github.com/EStarium/antares/commit/10b426b90b6b9461cfffce3026c982463f6e0599))
|
||||
* Lack of loading progressbar when an update is available ([86aec4f](https://github.com/EStarium/antares/commit/86aec4f5e41c059e88066a01f0d85155de99a5ee))
|
||||
* Missing schema when queryng INFORMATION_SCHEMA ([530d1bd](https://github.com/EStarium/antares/commit/530d1bd43fa95de05f594b9b5cae2f4b397f96e0))
|
||||
* Prevent multiple app instances ([12fbe8c](https://github.com/EStarium/antares/commit/12fbe8c1a03259648554f2a5c69b5abbedc18a48))
|
||||
* Several fix on data and query tabs ([530907d](https://github.com/EStarium/antares/commit/530907d097ac4d995e1bfcb02e6c890fd6007e21))
|
||||
* Unable to obtain fields informations for some queries ([43c7072](https://github.com/EStarium/antares/commit/43c7072c1c83a2455ae48a37be69b444b3eb6560))
|
||||
* Unable to obtain keyUsage informations when adding new row ([023c6a6](https://github.com/EStarium/antares/commit/023c6a633a7f268b1a97b748ad08d2416cc30ffe))
|
||||
* Value overridden when join tables with fields with same name ([78965d2](https://github.com/EStarium/antares/commit/78965d23e3efb7d8d6d110d79142966e57200757))
|
||||
* Wrong field names when join tables ([ad0bad8](https://github.com/EStarium/antares/commit/ad0bad8486c3d67ec14ec1aed3d8aff6cce9df87))
|
||||
* Wrong italian translation ([b29e07c](https://github.com/EStarium/antares/commit/b29e07c3b722aec7e78f3cef2e357a53cbcac474))
|
||||
* Wrong schema fetching table fields and key usage ([8e71f42](https://github.com/EStarium/antares/commit/8e71f42a28060fdfeeb81502b0759d0d11f5bcfd))
|
||||
* Wrong table and schema when more than one query in a tab ([4684b41](https://github.com/EStarium/antares/commit/4684b4114b9c9c253120292d7d164d7676011f86))
|
||||
|
||||
### [0.0.6](https://github.com/EStarium/antares/compare/v0.0.5...v0.0.6) (2020-09-03)
|
||||
|
||||
|
53
README.md
53
README.md
@@ -4,44 +4,59 @@
|
||||
|
||||
# Antares SQL Client
|
||||
|
||||
 [](https://travis-ci.com/EStarium/antares)  
|
||||
 [](https://travis-ci.com/Fabio286/antares)  
|
||||
|
||||
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
|
||||
My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
|
||||
|
||||
**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, 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/EStarium/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.
|
||||
🌟 Don't forget to **leave a star** if you appreciate this project.
|
||||
|
||||
## Philosophy
|
||||
|
||||
Why am I developing an SQL client when there are a lot of them on the market?
|
||||
The main goal is to develop a totally free, cross platform and open source alternative, empowered by JavaScript's ecosystem.
|
||||
The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem.
|
||||
An application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons or submenu.
|
||||
|
||||
## How to contribute
|
||||
|
||||
- [Translate Antares](https://github.com/EStarium/antares/wiki/Translate-Antares)
|
||||
- [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
|
||||
|
||||
## Roadmap
|
||||
## Current main features
|
||||
|
||||
- Multiple database connections at same time.
|
||||
- Database management (add/edit/delete).
|
||||
- Full tables management, including indexes and foreign keys.
|
||||
- Run queries on multiple tabs.
|
||||
- Query suggestions.
|
||||
- Native dark theme.
|
||||
- Multi language.
|
||||
- Auto updates.
|
||||
|
||||
## Coming soon
|
||||
|
||||
This is a roadmap with major features will come in near future.
|
||||
|
||||
- Improvements of query editor area.
|
||||
- Database management (add/edit/delete).
|
||||
- Tables management (add/edit/delete).
|
||||
- Stored procedures, views, schedulers and triggers support.
|
||||
- Users management (add/edit/delete).
|
||||
- Stored procedures, views, schedulers and trigger support.
|
||||
- Database tools.
|
||||
- Context menu shortcuts.
|
||||
- Keyboard shortcuts.
|
||||
- More secure password storage.
|
||||
- Database tools (variables, process list...).
|
||||
- SSL and SSH tunnel support.
|
||||
- Support for other databases.
|
||||
- UI/UX improvements.
|
||||
- Improvements of query editor area.
|
||||
- Improvements of query suggestions.
|
||||
- Query history.
|
||||
- More context menu shortcuts.
|
||||
- More keyboard shortcuts.
|
||||
- Query logs console.
|
||||
- Fake data filler.
|
||||
- Import/export and migration.
|
||||
- SSH tunnel.
|
||||
- Themes.
|
||||
|
||||
## Currently supported
|
||||
@@ -49,7 +64,7 @@ This is a roadmap with major features will come in near future.
|
||||
### Databases
|
||||
|
||||
- [x] MySQL/MariaDB
|
||||
- [ ] PostrgreSQL
|
||||
- [ ] PostgreSQL
|
||||
- [ ] MSSQL
|
||||
- [ ] SQLite
|
||||
- [ ] OracleDB
|
||||
@@ -57,7 +72,7 @@ This is a roadmap with major features will come in near future.
|
||||
|
||||
### Operating Systems
|
||||
|
||||
#### • x86
|
||||
#### • x64
|
||||
|
||||
- [x] Windows
|
||||
- [x] Linux
|
||||
@@ -71,6 +86,6 @@ This is a roadmap with major features will come in near future.
|
||||
|
||||
## Translations
|
||||
|
||||
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/EStarium/antares/pull/20)
|
||||
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/EStarium/antares/pull/29)
|
||||
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/EStarium/antares/pull/32)
|
||||
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/Fabio286/antares/pull/20)
|
||||
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/Fabio286/antares/pull/29)
|
||||
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/Fabio286/antares/pull/32)
|
||||
|
5
jsconfig.json
Normal file
5
jsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"include": [
|
||||
"./src/renderer/**/*"
|
||||
]
|
||||
}
|
44
package.json
44
package.json
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "antares",
|
||||
"productName": "Antares",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.11",
|
||||
"description": "A cross-platform easy to use SQL client.",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/EStarium/antares.git",
|
||||
"repository": "https://github.com/Fabio286/antares.git",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development electron-webpack dev",
|
||||
"compile": "electron-webpack",
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"author": "Fabio Di Stasio <fabio286@gmail.com>",
|
||||
"build": {
|
||||
"appId": "com.estarium.antares",
|
||||
"appId": "com.fabio286.antares",
|
||||
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
|
||||
"dmg": {
|
||||
"contents": [
|
||||
@@ -47,44 +47,42 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^5.6.55",
|
||||
"electron-log": "^4.2.4",
|
||||
"@mdi/font": "^5.8.55",
|
||||
"electron-log": "^4.3.0",
|
||||
"electron-updater": "^4.3.5",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.29.0",
|
||||
"moment": "^2.29.1",
|
||||
"monaco-editor": "^0.20.0",
|
||||
"mssql": "^6.2.2",
|
||||
"mssql": "^6.2.3",
|
||||
"mysql": "^2.18.1",
|
||||
"pg": "^8.3.3",
|
||||
"pg": "^8.5.1",
|
||||
"source-map-support": "^0.5.16",
|
||||
"spectre.css": "^0.5.9",
|
||||
"vue-click-outside": "^1.1.0",
|
||||
"vue-i18n": "^8.21.0",
|
||||
"vue-i18n": "^8.22.2",
|
||||
"vue-the-mask": "^0.11.1",
|
||||
"vuedraggable": "^2.24.1",
|
||||
"vuex": "^3.5.1",
|
||||
"vuex-persist": "^3.1.0"
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.0",
|
||||
"vuex-persist": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"electron": "^10.1.0",
|
||||
"electron-builder": "^22.8.1",
|
||||
"electron": "^11.0.2",
|
||||
"electron-builder": "^22.9.1",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-webpack": "^2.8.2",
|
||||
"electron-webpack-vue": "^2.4.0",
|
||||
"eslint": "^7.8.1",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint": "^7.14.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"eslint-plugin-vue": "^7.1.0",
|
||||
"monaco-editor-webpack-plugin": "^1.9.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"sass-loader": "^10.0.2",
|
||||
"node-sass": "^5.0.0",
|
||||
"sass-loader": "^10.1.0",
|
||||
"standard-version": "^9.0.0",
|
||||
"stylelint": "^13.7.0",
|
||||
"stylelint": "^13.8.0",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-scss": "^3.18.0",
|
||||
"vue": "^2.6.12",
|
||||
|
303
src/common/data-types/mysql.js
Normal file
303
src/common/data-types/mysql.js
Normal file
@@ -0,0 +1,303 @@
|
||||
module.exports = [
|
||||
{
|
||||
group: 'integer',
|
||||
types: [
|
||||
{
|
||||
name: 'TINYINT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
},
|
||||
{
|
||||
name: 'SMALLINT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
},
|
||||
{
|
||||
name: 'INT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
},
|
||||
{
|
||||
name: 'MEDIUMINT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
},
|
||||
{
|
||||
name: 'BIGINT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
},
|
||||
{
|
||||
name: 'BIT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: true,
|
||||
zerofill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'float',
|
||||
types: [
|
||||
{
|
||||
name: 'FLOAT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'DOUBLE',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'DECIMAL',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'string',
|
||||
types: [
|
||||
{
|
||||
name: 'CHAR',
|
||||
length: true,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'VARCHAR',
|
||||
length: true,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'TINYTEXT',
|
||||
length: true,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'MEDIUMTEXT',
|
||||
length: false,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'TEXT',
|
||||
length: false,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'LONGTEXT',
|
||||
length: false,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'JSON',
|
||||
length: true,
|
||||
collation: true,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'binary',
|
||||
types: [
|
||||
{
|
||||
name: 'BINARY',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'VARBINARY',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'TINYBLOB',
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'BLOB',
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'MEDIUMBLOB',
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'LONGBLOB',
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'time',
|
||||
types: [
|
||||
{
|
||||
name: 'DATE',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'TIME',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'YEAR',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'DATETIME',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'TIMESTAMP',
|
||||
length: false,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'spatial',
|
||||
types: [
|
||||
{
|
||||
name: 'POINT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'LINESTRING',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'POLYGON',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'GEOMETRY',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'MULTIPOINT',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'MULTILINESTRING',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'MULTIPOLYGON',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'GEOMETRYCOLLECTION',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'other',
|
||||
types: [
|
||||
{
|
||||
name: 'UNKNOWN',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'ENUM',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
},
|
||||
{
|
||||
name: 'SET',
|
||||
length: true,
|
||||
collation: false,
|
||||
unsigned: false,
|
||||
zerofill: false
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
@@ -1,12 +1,12 @@
|
||||
export const TEXT = ['char', 'varchar'];
|
||||
export const LONG_TEXT = ['text', 'mediumtext', 'longtext'];
|
||||
export const TEXT = ['CHAR', 'VARCHAR'];
|
||||
export const LONG_TEXT = ['TEXT', 'MEDIUMTEXT', 'longtext'];
|
||||
|
||||
export const NUMBER = ['int', 'tinyint', 'smallint', 'mediumint', 'bigint', 'float', 'double', 'decimal'];
|
||||
export const NUMBER = ['INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL', 'BOOL'];
|
||||
|
||||
export const DATE = ['date'];
|
||||
export const TIME = ['time'];
|
||||
export const DATETIME = ['datetime', 'timestamp'];
|
||||
export const DATE = ['DATE'];
|
||||
export const TIME = ['TIME'];
|
||||
export const DATETIME = ['DATETIME', 'TIMESTAMP'];
|
||||
|
||||
export const BLOB = ['blob', 'mediumblob', 'longblob'];
|
||||
export const BLOB = ['BLOB', 'MEDIUMBLOB', 'LONGBLOB'];
|
||||
|
||||
export const BIT = ['bit'];
|
||||
export const BIT = ['BIT'];
|
||||
|
6
src/common/index-types/mysql.js
Normal file
6
src/common/index-types/mysql.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = [
|
||||
'PRIMARY',
|
||||
'INDEX',
|
||||
'UNIQUE',
|
||||
'FULLTEXT'
|
||||
];
|
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const regex = new RegExp(/[\0\x08\x09\x1a\n\r"'\\\%]/gm);
|
||||
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
|
||||
const regex = new RegExp(pattern);
|
||||
|
||||
/**
|
||||
* Escapes a string
|
||||
@@ -10,8 +11,8 @@ const regex = new RegExp(/[\0\x08\x09\x1a\n\r"'\\\%]/gm);
|
||||
*/
|
||||
function sqlEscaper (string) {
|
||||
return string.replace(regex, char => {
|
||||
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '"', '\\', '\\\\', '%'];
|
||||
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\'\'', '""', '\\\\', '\\\\\\\\', '\\%'];
|
||||
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
|
||||
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\\%'];
|
||||
return r[m.indexOf(char)] || char;
|
||||
});
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ export default connections => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-database-collation', async (event, params) => {
|
||||
ipcMain.handle('get-database-collation', async (event, params) => { // TODO: move to mysql class
|
||||
try {
|
||||
const query = `SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`;
|
||||
const collation = await connections[params.uid].raw(query);
|
||||
@@ -83,6 +83,17 @@ export default connections => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-engines', async (event, uid) => {
|
||||
try {
|
||||
const result = await connections[uid].getEngines();
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('use-schema', async (event, { uid, schema }) => {
|
||||
if (!schema) return;
|
||||
|
||||
@@ -99,7 +110,7 @@ export default connections => {
|
||||
if (!query) return;
|
||||
|
||||
try {
|
||||
const result = await connections[uid].raw(query, true);
|
||||
const result = await connections[uid].raw(query, { nest: true, details: true });
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
|
@@ -3,37 +3,10 @@ import { sqlEscaper } from 'common/libs/sqlEscaper';
|
||||
import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes';
|
||||
import fs from 'fs';
|
||||
|
||||
// TODO: remap objects based on client
|
||||
|
||||
export default (connections) => {
|
||||
ipcMain.handle('get-table-columns', async (event, { uid, schema, table }) => {
|
||||
ipcMain.handle('get-table-columns', async (event, params) => {
|
||||
try {
|
||||
const { rows } = await connections[uid]
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('COLUMNS')
|
||||
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` })
|
||||
.orderBy({ ORDINAL_POSITION: 'ASC' })
|
||||
.run();
|
||||
|
||||
const result = rows.map(field => {
|
||||
return {
|
||||
name: field.COLUMN_NAME,
|
||||
key: field.COLUMN_KEY.toLowerCase(),
|
||||
type: field.DATA_TYPE,
|
||||
schema: field.TABLE_SCHEMA,
|
||||
table: field.TABLE_NAME,
|
||||
numPrecision: field.NUMERIC_PRECISION,
|
||||
datePrecision: field.DATETIME_PRECISION,
|
||||
charLength: field.CHARACTER_MAXIMUM_LENGTH,
|
||||
isNullable: field.IS_NULLABLE,
|
||||
default: field.COLUMN_DEFAULT,
|
||||
charset: field.CHARACTER_SET_NAME,
|
||||
collation: field.COLLATION_NAME,
|
||||
autoIncrement: field.EXTRA.includes('auto_increment'),
|
||||
comment: field.COLUMN_COMMENT
|
||||
};
|
||||
});
|
||||
const result = await connections[params.uid].getTableColumns(params);
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
@@ -41,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 {
|
||||
const result = await connections[uid]
|
||||
const query = connections[uid]
|
||||
.select('*')
|
||||
.schema(schema)
|
||||
.from(table)
|
||||
.limit(1000)
|
||||
.run();
|
||||
.limit(1000);
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -57,28 +34,20 @@ export default (connections) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-key-usage', async (event, { uid, schema, table }) => {
|
||||
ipcMain.handle('get-table-indexes', async (event, params) => {
|
||||
try {
|
||||
const { rows } = await connections[uid]
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('KEY_COLUMN_USAGE')
|
||||
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
|
||||
.run();
|
||||
const result = await connections[params.uid].getTableIndexes(params);
|
||||
|
||||
const result = rows.map(field => {
|
||||
return {
|
||||
schema: field.TABLE_SCHEMA,
|
||||
table: field.TABLE_NAME,
|
||||
column: field.COLUMN_NAME,
|
||||
position: field.ORDINAL_POSITION,
|
||||
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
|
||||
constraintName: field.CONSTRAINT_NAME,
|
||||
refSchema: field.REFERENCED_TABLE_SCHEMA,
|
||||
refTable: field.REFERENCED_TABLE_NAME,
|
||||
refColumn: field.REFERENCED_COLUMN_NAME
|
||||
};
|
||||
});
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-key-usage', async (event, params) => {
|
||||
try {
|
||||
const result = await connections[params.uid].getKeyUsage(params);
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
@@ -124,11 +93,18 @@ export default (connections) => {
|
||||
});
|
||||
|
||||
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 {
|
||||
const result = await connections[params.uid]
|
||||
.schema(params.schema)
|
||||
.delete(params.table)
|
||||
.where({ [params.primary]: `IN (${params.rows.join(',')})` })
|
||||
.where({ [params.primary]: `IN (${idString})` })
|
||||
.run();
|
||||
|
||||
return { status: 'success', response: result };
|
||||
@@ -180,16 +156,16 @@ export default (connections) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-foreign-list', async (event, params) => {
|
||||
ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => {
|
||||
try {
|
||||
const query = connections[params.uid]
|
||||
.select(`${params.column} AS foreignColumn`)
|
||||
.schema(params.schema)
|
||||
.from(params.table)
|
||||
const query = connections[uid]
|
||||
.select(`${column} AS foreignColumn`)
|
||||
.schema(schema)
|
||||
.from(table)
|
||||
.orderBy('foreignColumn ASC');
|
||||
|
||||
if (params.description)
|
||||
query.select(`LEFT(${params.description}, 20) AS foreignDescription`);
|
||||
if (description)
|
||||
query.select(`LEFT(${description}, 20) AS foreignDescription`);
|
||||
|
||||
const results = await query.run();
|
||||
|
||||
@@ -199,4 +175,44 @@ export default (connections) => {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('create-table', async (event, params) => {
|
||||
try {
|
||||
await connections[params.uid].createTable(params);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('alter-table', async (event, params) => {
|
||||
try {
|
||||
await connections[params.uid].alterTable(params);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('truncate-table', async (event, params) => {
|
||||
try {
|
||||
await connections[params.uid].truncateTable(params);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('drop-table', async (event, params) => {
|
||||
try {
|
||||
await connections[params.uid].dropTable(params);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -130,12 +130,13 @@ export class AntaresCore {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @returns {Promise}
|
||||
* @memberof AntaresCore
|
||||
*/
|
||||
async run () {
|
||||
async run (args) {
|
||||
const rawQuery = this.getSQL();
|
||||
this._resetQuery();
|
||||
return this.raw(rawQuery);
|
||||
return this.raw(rawQuery, args);
|
||||
}
|
||||
}
|
||||
|
@@ -61,14 +61,203 @@ export class MySQLClient extends AntaresCore {
|
||||
}
|
||||
}
|
||||
|
||||
return databases.map(db => { // TODO: remap all objects,
|
||||
return databases.map(db => {
|
||||
// TABLES
|
||||
const remappedTables = tables.filter(table => table.TABLE_SCHEMA === db.Database).map(table => {
|
||||
let tableType;
|
||||
switch (table.TABLE_TYPE) {
|
||||
case 'VIEW':
|
||||
tableType = 'view';
|
||||
break;
|
||||
default:
|
||||
tableType = 'table';
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
name: table.TABLE_NAME,
|
||||
type: tableType,
|
||||
rows: table.TABLE_ROWS,
|
||||
created: table.CREATE_TIME,
|
||||
updated: table.UPDATE_TIME,
|
||||
engine: table.ENGINE,
|
||||
comment: table.TABLE_COMMENT,
|
||||
size: table.DATA_LENGTH + table.INDEX_LENGTH,
|
||||
autoIncrement: table.AUTO_INCREMENT,
|
||||
collation: table.TABLE_COLLATION
|
||||
};
|
||||
});
|
||||
|
||||
// PROCEDURES
|
||||
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
|
||||
return {
|
||||
name: procedure.Name,
|
||||
type: procedure.Type,
|
||||
definer: procedure.Definer,
|
||||
created: procedure.Created,
|
||||
updated: procedure.Modified,
|
||||
comment: procedure.Comment,
|
||||
charset: procedure.character_set_client,
|
||||
security: procedure.Security_type
|
||||
};
|
||||
});
|
||||
|
||||
// SCHEDULERS
|
||||
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
|
||||
return {
|
||||
name: scheduler.EVENT_NAME,
|
||||
definition: scheduler.EVENT_DEFINITION,
|
||||
type: scheduler.EVENT_TYPE,
|
||||
definer: scheduler.DEFINER,
|
||||
body: scheduler.EVENT_BODY,
|
||||
starts: scheduler.STARTS,
|
||||
ends: scheduler.ENDS,
|
||||
status: scheduler.STATUS,
|
||||
executeAt: scheduler.EXECUTE_AT,
|
||||
intervalField: scheduler.INTERVAL_FIELD,
|
||||
intervalValue: scheduler.INTERVAL_VALUE,
|
||||
onCompletion: scheduler.ON_COMPLETION,
|
||||
originator: scheduler.ORIGINATOR,
|
||||
sqlMode: scheduler.SQL_MODE,
|
||||
created: scheduler.CREATED,
|
||||
updated: scheduler.LAST_ALTERED,
|
||||
lastExecuted: scheduler.LAST_EXECUTED,
|
||||
comment: scheduler.EVENT_COMMENT,
|
||||
charset: scheduler.CHARACTER_SET_CLIENT,
|
||||
timezone: scheduler.TIME_ZONE
|
||||
};
|
||||
});
|
||||
|
||||
// TRIGGERS
|
||||
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => {
|
||||
return {
|
||||
name: trigger.Trigger,
|
||||
statement: trigger.Statement,
|
||||
timing: trigger.Timing,
|
||||
definer: trigger.Definer,
|
||||
event: trigger.Event,
|
||||
table: trigger.Table,
|
||||
sqlMode: trigger.sql_mode,
|
||||
created: trigger.Created,
|
||||
charset: trigger.character_set_client
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: db.Database,
|
||||
tables: tables.filter(table => table.TABLE_SCHEMA === db.Database),
|
||||
functions: functions.filter(func => func.Db === db.Database),
|
||||
procedures: procedures.filter(procedure => procedure.Db === db.Database),
|
||||
triggers: triggersArr.filter(trigger => trigger.Db === db.Database),
|
||||
schedulers: schedulers.filter(scheduler => scheduler.Db === db.Database)
|
||||
tables: remappedTables,
|
||||
functions: functions.filter(func => func.Db === db.Database), // TODO: remap functions
|
||||
procedures: remappedProcedures,
|
||||
triggers: remappedTriggers,
|
||||
schedulers: remappedSchedulers
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table scructure
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getTableColumns ({ schema, table }) {
|
||||
const { rows } = await this
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('COLUMNS')
|
||||
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` })
|
||||
.orderBy({ ORDINAL_POSITION: 'ASC' })
|
||||
.run();
|
||||
|
||||
return rows.map(field => {
|
||||
let numLength = field.COLUMN_TYPE.match(/int\(([^)]+)\)/);
|
||||
numLength = numLength ? +numLength.pop() : null;
|
||||
|
||||
return {
|
||||
name: field.COLUMN_NAME,
|
||||
key: field.COLUMN_KEY.toLowerCase(),
|
||||
type: field.DATA_TYPE.toUpperCase(),
|
||||
schema: field.TABLE_SCHEMA,
|
||||
table: field.TABLE_NAME,
|
||||
numPrecision: field.NUMERIC_PRECISION,
|
||||
numLength,
|
||||
datePrecision: field.DATETIME_PRECISION,
|
||||
charLength: field.CHARACTER_MAXIMUM_LENGTH,
|
||||
nullable: field.IS_NULLABLE.includes('YES'),
|
||||
unsigned: field.COLUMN_TYPE.includes('unsigned'),
|
||||
zerofill: field.COLUMN_TYPE.includes('zerofill'),
|
||||
order: field.ORDINAL_POSITION,
|
||||
default: field.COLUMN_DEFAULT,
|
||||
charset: field.CHARACTER_SET_NAME,
|
||||
collation: field.COLLATION_NAME,
|
||||
autoIncrement: field.EXTRA.includes('auto_increment'),
|
||||
onUpdate: field.EXTRA.toLowerCase().includes('on update') ? field.EXTRA.replace('on update', '') : '',
|
||||
comment: field.COLUMN_COMMENT
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table indexes
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getTableIndexes ({ schema, table }) {
|
||||
const { rows } = await this.raw(`SHOW INDEXES FROM \`${table}\` FROM \`${schema}\``);
|
||||
|
||||
return rows.map(row => {
|
||||
return {
|
||||
unique: !row.Non_unique,
|
||||
name: row.Key_name,
|
||||
column: row.Column_name,
|
||||
indexType: row.Index_type,
|
||||
type: row.Key_name === 'PRIMARY' ? 'PRIMARY' : !row.Non_unique ? 'UNIQUE' : row.Index_type === 'FULLTEXT' ? 'FULLTEXT' : 'INDEX',
|
||||
cardinality: row.Cardinality,
|
||||
comment: row.Comment,
|
||||
indexComment: row.Index_comment
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table key usage
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getKeyUsage ({ schema, table }) {
|
||||
const { rows } = await this
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('KEY_COLUMN_USAGE')
|
||||
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
|
||||
.run();
|
||||
|
||||
const { rows: extras } = await this
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('REFERENTIAL_CONSTRAINTS')
|
||||
.where({ CONSTRAINT_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
|
||||
.run();
|
||||
|
||||
return rows.map(field => {
|
||||
const extra = extras.find(x => x.CONSTRAINT_NAME === field.CONSTRAINT_NAME);
|
||||
return {
|
||||
schema: field.TABLE_SCHEMA,
|
||||
table: field.TABLE_NAME,
|
||||
field: field.COLUMN_NAME,
|
||||
position: field.ORDINAL_POSITION,
|
||||
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
|
||||
constraintName: field.CONSTRAINT_NAME,
|
||||
refSchema: field.REFERENCED_TABLE_SCHEMA,
|
||||
refTable: field.REFERENCED_TABLE_NAME,
|
||||
refField: field.REFERENCED_COLUMN_NAME,
|
||||
onUpdate: extra.UPDATE_RULE,
|
||||
onDelete: extra.DELETE_RULE
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -112,6 +301,202 @@ export class MySQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW ENGINES
|
||||
*
|
||||
* @returns {Array.<Object>} engines list
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getEngines () {
|
||||
const sql = 'SHOW ENGINES';
|
||||
const results = await this.raw(sql);
|
||||
|
||||
return results.rows.map(row => {
|
||||
return {
|
||||
name: row.Engine,
|
||||
support: row.Support,
|
||||
comment: row.Comment,
|
||||
transactions: row.Transactions,
|
||||
xa: row.XA,
|
||||
savepoints: row.Savepoints,
|
||||
isDefault: row.Support.includes('DEFAULT')
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async createTable (params) {
|
||||
const {
|
||||
name,
|
||||
collation,
|
||||
comment,
|
||||
engine
|
||||
} = params;
|
||||
|
||||
const sql = `CREATE TABLE \`${name}\` (\`${name}_ID\` INT NULL) COMMENT='${comment}', COLLATE='${collation}', ENGINE=${engine}`;
|
||||
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async alterTable (params) {
|
||||
const {
|
||||
table,
|
||||
additions,
|
||||
deletions,
|
||||
changes,
|
||||
indexChanges,
|
||||
foreignChanges,
|
||||
options
|
||||
} = params;
|
||||
|
||||
let sql = `ALTER TABLE \`${table}\` `;
|
||||
const alterColumns = [];
|
||||
|
||||
// OPTIONS
|
||||
if ('comment' in options) alterColumns.push(`COMMENT='${options.comment}'`);
|
||||
if ('engine' in options) alterColumns.push(`ENGINE=${options.engine}`);
|
||||
if ('autoIncrement' in options) alterColumns.push(`AUTO_INCREMENT=${+options.autoIncrement}`);
|
||||
if ('collation' in options) alterColumns.push(`COLLATE='${options.collation}'`);
|
||||
|
||||
// ADD FIELDS
|
||||
additions.forEach(addition => {
|
||||
const length = addition.numLength || addition.charLength || addition.datePrecision;
|
||||
|
||||
alterColumns.push(`ADD COLUMN \`${addition.name}\`
|
||||
${addition.type.toUpperCase()}${length ? `(${length})` : ''}
|
||||
${addition.unsigned ? 'UNSIGNED' : ''}
|
||||
${addition.zerofill ? 'ZEROFILL' : ''}
|
||||
${addition.nullable ? 'NULL' : 'NOT NULL'}
|
||||
${addition.autoIncrement ? 'AUTO_INCREMENT' : ''}
|
||||
${addition.default ? `DEFAULT ${addition.default}` : ''}
|
||||
${addition.comment ? `COMMENT '${addition.comment}'` : ''}
|
||||
${addition.collation ? `COLLATE ${addition.collation}` : ''}
|
||||
${addition.onUpdate ? `ON UPDATE ${addition.onUpdate}` : ''}
|
||||
${addition.after ? `AFTER \`${addition.after}\`` : 'FIRST'}`);
|
||||
});
|
||||
|
||||
// ADD INDEX
|
||||
indexChanges.additions.forEach(addition => {
|
||||
const fields = addition.fields.map(field => `\`${field}\``).join(',');
|
||||
let type = addition.type;
|
||||
|
||||
if (type === 'PRIMARY')
|
||||
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
|
||||
else {
|
||||
if (type === 'UNIQUE')
|
||||
type = 'UNIQUE INDEX';
|
||||
|
||||
alterColumns.push(`ADD ${type} \`${addition.name}\` (${fields})`);
|
||||
}
|
||||
});
|
||||
|
||||
// ADD FOREIGN KEYS
|
||||
foreignChanges.additions.forEach(addition => {
|
||||
alterColumns.push(`ADD CONSTRAINT \`${addition.constraintName}\` FOREIGN KEY (\`${addition.field}\`) REFERENCES \`${addition.refTable}\` (\`${addition.refField}\`) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
|
||||
});
|
||||
|
||||
// CHANGE FIELDS
|
||||
changes.forEach(change => {
|
||||
const length = change.numLength || change.charLength || change.datePrecision;
|
||||
|
||||
alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\`
|
||||
${change.type.toUpperCase()}${length ? `(${length})` : ''}
|
||||
${change.unsigned ? 'UNSIGNED' : ''}
|
||||
${change.zerofill ? 'ZEROFILL' : ''}
|
||||
${change.nullable ? 'NULL' : 'NOT NULL'}
|
||||
${change.autoIncrement ? 'AUTO_INCREMENT' : ''}
|
||||
${change.default ? `DEFAULT ${change.default}` : ''}
|
||||
${change.comment ? `COMMENT '${change.comment}'` : ''}
|
||||
${change.collation ? `COLLATE ${change.collation}` : ''}
|
||||
${change.onUpdate ? `ON UPDATE ${change.onUpdate}` : ''}
|
||||
${change.after ? `AFTER \`${change.after}\`` : 'FIRST'}`);
|
||||
});
|
||||
|
||||
// CHANGE INDEX
|
||||
indexChanges.changes.forEach(change => {
|
||||
if (change.oldType === 'PRIMARY')
|
||||
alterColumns.push('DROP PRIMARY KEY');
|
||||
else
|
||||
alterColumns.push(`DROP INDEX \`${change.oldName}\``);
|
||||
|
||||
const fields = change.fields.map(field => `\`${field}\``).join(',');
|
||||
let type = change.type;
|
||||
|
||||
if (type === 'PRIMARY')
|
||||
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
|
||||
else {
|
||||
if (type === 'UNIQUE')
|
||||
type = 'UNIQUE INDEX';
|
||||
|
||||
alterColumns.push(`ADD ${type} \`${change.name}\` (${fields})`);
|
||||
}
|
||||
});
|
||||
|
||||
// CHANGE FOREIGN KEYS
|
||||
foreignChanges.changes.forEach(change => {
|
||||
alterColumns.push(`DROP FOREIGN KEY \`${change.oldName}\``);
|
||||
alterColumns.push(`ADD CONSTRAINT \`${change.constraintName}\` FOREIGN KEY (\`${change.field}\`) REFERENCES \`${change.refTable}\` (\`${change.refField}\`) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
|
||||
});
|
||||
|
||||
// DROP FIELDS
|
||||
deletions.forEach(deletion => {
|
||||
alterColumns.push(`DROP COLUMN \`${deletion.name}\``);
|
||||
});
|
||||
|
||||
// DROP INDEX
|
||||
indexChanges.deletions.forEach(deletion => {
|
||||
if (deletion.type === 'PRIMARY')
|
||||
alterColumns.push('DROP PRIMARY KEY');
|
||||
else
|
||||
alterColumns.push(`DROP INDEX \`${deletion.name}\``);
|
||||
});
|
||||
|
||||
// DROP FOREIGN KEYS
|
||||
foreignChanges.deletions.forEach(deletion => {
|
||||
alterColumns.push(`DROP FOREIGN KEY \`${deletion.constraintName}\``);
|
||||
});
|
||||
|
||||
sql += alterColumns.join(', ');
|
||||
|
||||
// RENAME
|
||||
if (options.name) sql += `; RENAME TABLE \`${table}\` TO \`${options.name}\``;
|
||||
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* TRUNCATE TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async truncateTable (params) {
|
||||
const sql = `TRUNCATE TABLE \`${params.table}\``;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* DROP TABLE
|
||||
*
|
||||
* @returns {Array.<Object>} parameters
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async dropTable (params) {
|
||||
const sql = `DROP TABLE \`${params.table}\``;
|
||||
return await this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {String} SQL string
|
||||
* @memberof MySQLClient
|
||||
@@ -175,34 +560,129 @@ export class MySQLClient extends AntaresCore {
|
||||
|
||||
/**
|
||||
* @param {string} sql raw SQL query
|
||||
* @param {boolean} [nest]
|
||||
* @param {object} args
|
||||
* @param {boolean} args.nest
|
||||
* @param {boolean} args.details
|
||||
* @returns {Promise}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async raw (sql, nest) {
|
||||
const nestTables = nest ? '.' : false;
|
||||
async raw (sql, args) {
|
||||
args = {
|
||||
nest: false,
|
||||
details: false,
|
||||
...args
|
||||
};
|
||||
const nestTables = args.nest ? '.' : false;
|
||||
const resultsArr = [];
|
||||
let paramsArr = [];
|
||||
let selectedFields = [];
|
||||
const queries = sql.split(';');
|
||||
|
||||
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
|
||||
|
||||
for (const query of queries) {
|
||||
if (!query) continue;
|
||||
let fieldsArr = [];
|
||||
let keysArr = [];
|
||||
|
||||
const { rows, report, fields, keys } = await new Promise((resolve, reject) => {
|
||||
this._connection.query({ sql: query, nestTables }, async (err, response, fields) => {
|
||||
const queryResult = response;
|
||||
|
||||
const { rows, report, fields } = await new Promise((resolve, reject) => {
|
||||
this._connection.query({ sql: query, nestTables }, (err, response, fields) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else {
|
||||
const remappedFields = fields
|
||||
? fields.map(field => {
|
||||
return {
|
||||
name: field.name,
|
||||
orgName: field.orgName,
|
||||
schema: field.db,
|
||||
table: field.table,
|
||||
orgTable: field.orgTable,
|
||||
type: 'varchar'
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
if (args.details) {
|
||||
let cachedTable;
|
||||
|
||||
if (remappedFields.length) {
|
||||
selectedFields = remappedFields.map(field => {
|
||||
return {
|
||||
name: field.orgName || field.name,
|
||||
table: field.orgTable || field.table
|
||||
};
|
||||
});
|
||||
|
||||
paramsArr = remappedFields.map(field => {
|
||||
if (field.table) cachedTable = field.table;// Needed for some queries on information_schema
|
||||
return {
|
||||
table: field.orgTable || cachedTable,
|
||||
schema: field.schema || 'INFORMATION_SCHEMA'
|
||||
};
|
||||
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
|
||||
|
||||
for (const paramObj of paramsArr) {
|
||||
try { // Table data
|
||||
const response = await this.getTableColumns(paramObj);
|
||||
|
||||
let detailedFields = response.length
|
||||
? selectedFields.map(selField => {
|
||||
return response.find(field => field.name === selField.name && field.table === selField.table);
|
||||
}).filter(el => !!el)
|
||||
: [];
|
||||
|
||||
if (selectedFields.length) {
|
||||
detailedFields = detailedFields.map(field => {
|
||||
const aliasObj = remappedFields.find(resField => resField.orgName === field.name);
|
||||
return {
|
||||
...field,
|
||||
alias: aliasObj.name || field.name,
|
||||
tableAlias: aliasObj.table || field.table
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (!detailedFields.length) {
|
||||
detailedFields = remappedFields.map(field => {
|
||||
return {
|
||||
...field,
|
||||
alias: field.name,
|
||||
tableAlias: field.table
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fieldsArr = fieldsArr ? [...fieldsArr, ...detailedFields] : detailedFields;
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
try { // Key usage (foreign keys)
|
||||
const response = await this.getKeyUsage(paramObj);
|
||||
keysArr = keysArr ? [...keysArr, ...response] : response;
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
rows: Array.isArray(response) ? response : false,
|
||||
report: !Array.isArray(response) ? response : false,
|
||||
fields
|
||||
rows: Array.isArray(queryResult) ? queryResult : false,
|
||||
report: !Array.isArray(queryResult) ? queryResult : false,
|
||||
fields: fieldsArr.length ? fieldsArr : remappedFields,
|
||||
keys: keysArr
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
resultsArr.push({ rows, report, fields });
|
||||
|
||||
resultsArr.push({ rows, report, fields, keys });
|
||||
}
|
||||
|
||||
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
|
||||
|
@@ -16,8 +16,8 @@
|
||||
<TheFooter />
|
||||
<TheNotificationsBoard />
|
||||
<ModalNewConnection v-if="isNewConnModal" />
|
||||
<ModalEditConnection v-if="isEditModal" />
|
||||
<ModalSettings v-if="isSettingModal" />
|
||||
<ModalDiscardChanges v-if="isUnsavedDiscardModal" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,8 +36,8 @@ export default {
|
||||
TheAppWelcome: () => import(/* webpackChunkName: "TheAppWelcome" */'@/components/TheAppWelcome'),
|
||||
Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'),
|
||||
ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'),
|
||||
ModalEditConnection: () => import(/* webpackChunkName: "ModalEditConnection" */'@/components/ModalEditConnection'),
|
||||
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')
|
||||
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'),
|
||||
ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges')
|
||||
},
|
||||
data () {
|
||||
return {};
|
||||
@@ -48,7 +48,8 @@ export default {
|
||||
isNewConnModal: 'application/isNewModal',
|
||||
isEditModal: 'application/isEditModal',
|
||||
isSettingModal: 'application/isSettingModal',
|
||||
connections: 'connections/getConnections'
|
||||
connections: 'connections/getConnections',
|
||||
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal'
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
|
@@ -48,7 +48,7 @@ export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
validator: prop => ['small', 'medium', 'large'].includes(prop),
|
||||
validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
|
||||
default: 'small'
|
||||
},
|
||||
confirmText: String,
|
||||
@@ -67,6 +67,8 @@ export default {
|
||||
modalSizeClass () {
|
||||
if (this.size === 'small')
|
||||
return 'modal-sm';
|
||||
if (this.size === '400')
|
||||
return 'modal-400';
|
||||
else if (this.size === 'large')
|
||||
return 'modal-lg';
|
||||
else return '';
|
||||
@@ -86,7 +88,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal.modal-sm .modal-container {
|
||||
padding: 0;
|
||||
}
|
||||
.modal-400 .modal-container {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.modal.modal-sm .modal-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div class="context">
|
||||
<a
|
||||
class="context-overlay"
|
||||
@click="close"
|
||||
@contextmenu="close"
|
||||
/>
|
||||
<div
|
||||
v-click-outside="close"
|
||||
ref="contextContent"
|
||||
class="context-container"
|
||||
:style="position"
|
||||
>
|
||||
@@ -11,27 +16,53 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClickOutside from 'vue-click-outside';
|
||||
|
||||
export default {
|
||||
name: 'BaseContextMenu',
|
||||
directives: {
|
||||
ClickOutside
|
||||
},
|
||||
props: {
|
||||
contextEvent: MouseEvent
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
contextSize: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
position () {
|
||||
return { // TODO: calc direction if near corners
|
||||
top: this.contextEvent.clientY + 5 + 'px',
|
||||
left: this.contextEvent.clientX + 5 + 'px'
|
||||
const { clientY, clientX } = this.contextEvent;
|
||||
let topCord = `${clientY + 5}px`;
|
||||
let leftCord = `${clientX + 5}px`;
|
||||
|
||||
if (this.contextSize) {
|
||||
if (clientY + this.contextSize.height + 5 >= window.innerHeight)
|
||||
topCord = `${clientY - this.contextSize.height}px`;
|
||||
|
||||
if (clientX + this.contextSize.width + 5 >= window.innerWidth)
|
||||
leftCord = `${clientX - this.contextSize.width}px`;
|
||||
}
|
||||
|
||||
return {
|
||||
top: topCord,
|
||||
left: leftCord
|
||||
};
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
},
|
||||
mounted () {
|
||||
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$emit('close-context');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -52,13 +83,11 @@ export default {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.context-container {
|
||||
min-width: 100px;
|
||||
max-width: 150px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 0 1px 0 #000;
|
||||
box-shadow: 0 0 2px 0 #000;
|
||||
padding: 0;
|
||||
background: #1d1d1d;
|
||||
border-radius: 0.1rem;
|
||||
@@ -73,9 +102,28 @@ export default {
|
||||
padding: 0.1rem 0.3rem;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
||||
.context-submenu {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<select
|
||||
ref="editField"
|
||||
class="px-1"
|
||||
class="form-select pl-1 pr-4"
|
||||
:class="{'small-select': size === 'small'}"
|
||||
@change="onChange"
|
||||
@blur="$emit('blur')"
|
||||
>
|
||||
<option v-if="!isValidDefault" :value="value">
|
||||
{{ value }} - {{ $t('message.invalidDefault') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="row in foreignList"
|
||||
:key="row.foreignColumn"
|
||||
@@ -30,7 +34,11 @@ export default {
|
||||
},
|
||||
props: {
|
||||
value: [String, Number],
|
||||
keyUsage: Object
|
||||
keyUsage: Object,
|
||||
size: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -40,7 +48,10 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected'
|
||||
})
|
||||
}),
|
||||
isValidDefault () {
|
||||
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString());
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
let firstTextField;
|
||||
@@ -64,7 +75,7 @@ export default {
|
||||
try { // Foregn list
|
||||
const { status, response } = await Tables.getForeignList({
|
||||
...params,
|
||||
column: this.keyUsage.refColumn,
|
||||
column: this.keyUsage.refField,
|
||||
description: firstTextField
|
||||
});
|
||||
|
||||
|
@@ -19,6 +19,7 @@
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="credentials.user"
|
||||
class="form-input"
|
||||
type="text"
|
||||
@@ -63,6 +64,11 @@ export default {
|
||||
}
|
||||
};
|
||||
},
|
||||
created () {
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$emit('close-asking');
|
||||
|
57
src/renderer/components/ModalDiscardChanges.vue
Normal file
57
src/renderer/components/ModalDiscardChanges.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
:confirm-text="$t('word.discard')"
|
||||
:cancel-text="$t('word.stay')"
|
||||
@confirm="discardUnsavedChanges"
|
||||
@hide="closeUnsavedChangesModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-content-save-alert mr-1" /> {{ $t('message.unsavedChanges') }}
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<div>
|
||||
{{ $t('message.discardUnsavedChanges') }}
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'ModalDiscardChanges',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
discardUnsavedChanges: 'workspaces/discardUnsavedChanges',
|
||||
closeUnsavedChangesModal: 'workspaces/closeUnsavedChangesModal'
|
||||
}),
|
||||
closeModal () {
|
||||
this.$emit('close');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-container {
|
||||
max-width: 360px;
|
||||
}
|
||||
</style>
|
@@ -20,6 +20,7 @@
|
||||
</div>
|
||||
<div class="col-8 col-sm-12">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="localConnection.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
@@ -144,7 +145,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { mapActions } from 'vuex';
|
||||
import Connection from '@/ipc-api/Connection';
|
||||
import ModalAskCredentials from '@/components/ModalAskCredentials';
|
||||
import BaseToast from '@/components/BaseToast';
|
||||
@@ -155,6 +156,9 @@ export default {
|
||||
ModalAskCredentials,
|
||||
BaseToast
|
||||
},
|
||||
props: {
|
||||
connection: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
toast: {
|
||||
@@ -166,17 +170,19 @@ export default {
|
||||
localConnection: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
connection: 'application/getSelectedConnection'
|
||||
})
|
||||
},
|
||||
created () {
|
||||
this.localConnection = Object.assign({}, this.connection);
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
closeModal: 'application/hideEditConnModal',
|
||||
editConnection: 'connections/editConnection'
|
||||
}),
|
||||
async startTest () {
|
||||
@@ -226,6 +232,14 @@ export default {
|
||||
closeAsking () {
|
||||
this.isTesting = false;
|
||||
this.isAsking = false;
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -33,7 +33,11 @@
|
||||
<label class="form-label">{{ $t('word.collation') }}:</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<select v-model="database.collation" class="form-select">
|
||||
<select
|
||||
ref="firstInput"
|
||||
v-model="database.collation"
|
||||
class="form-select"
|
||||
>
|
||||
<option
|
||||
v-for="collation in collations"
|
||||
:key="collation.id"
|
||||
@@ -112,6 +116,15 @@ export default {
|
||||
collation: actualCollation || this.defaultCollation,
|
||||
prevCollation: actualCollation || this.defaultCollation
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
@@ -139,6 +152,11 @@ export default {
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -20,6 +20,7 @@
|
||||
</div>
|
||||
<div class="col-8 col-sm-12">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="connection.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
@@ -180,6 +181,16 @@ export default {
|
||||
isAsking: false
|
||||
};
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
closeModal: 'application/hideNewConnModal',
|
||||
@@ -249,6 +260,11 @@ export default {
|
||||
closeAsking () {
|
||||
this.isAsking = false;
|
||||
this.isTesting = false;
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -19,6 +19,7 @@
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<input
|
||||
ref="firstInput"
|
||||
v-model="database.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
@@ -88,6 +89,13 @@ export default {
|
||||
},
|
||||
created () {
|
||||
this.database = { ...this.database, collation: this.defaultCollation };
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
@@ -113,6 +121,11 @@ export default {
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close');
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
127
src/renderer/components/ModalNewTable.vue
Normal file
127
src/renderer/components/ModalNewTable.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
:confirm-text="$t('word.confirm')"
|
||||
size="400"
|
||||
@confirm="confirmOptionsChange"
|
||||
@hide="$emit('close')"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-table-plus mr-1" /> {{ $t('message.createNewTable') }}
|
||||
</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="localOptions.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.comment') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="localOptions.comment"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.collation') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="localOptions.collation" class="form-select">
|
||||
<option
|
||||
v-for="collation in workspace.collations"
|
||||
:key="collation.id"
|
||||
:value="collation.collation"
|
||||
>
|
||||
{{ collation.collation }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.engine') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="localOptions.engine" class="form-select">
|
||||
<option
|
||||
v-for="engine in workspace.engines"
|
||||
:key="engine.name"
|
||||
:value="engine.name"
|
||||
>
|
||||
{{ engine.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewTable',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
table: String,
|
||||
workspace: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localOptions: {
|
||||
name: '',
|
||||
comment: '',
|
||||
collation: '',
|
||||
engine: ''
|
||||
},
|
||||
isOptionsChanging: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
||||
}),
|
||||
defaultCollation () {
|
||||
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
|
||||
},
|
||||
defaultEngine () {
|
||||
return this.workspace.engines.find(engine => engine.isDefault).name;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.localOptions.collation = this.defaultCollation;
|
||||
this.localOptions.engine = this.defaultEngine;
|
||||
|
||||
setTimeout(() => {
|
||||
this.$refs.firstInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
methods: {
|
||||
confirmOptionsChange () {
|
||||
this.$emit('open-create-table-editor', this.localOptions);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -25,6 +25,7 @@
|
||||
<div class="input-group col-8 col-sm-12">
|
||||
<ForeignKeySelect
|
||||
v-if="foreignKeys.includes(field.name)"
|
||||
ref="formInput"
|
||||
class="form-select"
|
||||
:value.sync="localRow[field.name]"
|
||||
:key-usage="getKeyUsage(field.name)"
|
||||
@@ -32,6 +33,7 @@
|
||||
/>
|
||||
<input
|
||||
v-else-if="inputProps(field).mask"
|
||||
ref="formInput"
|
||||
v-model="localRow[field.name]"
|
||||
v-mask="inputProps(field).mask"
|
||||
class="form-input"
|
||||
@@ -41,6 +43,7 @@
|
||||
>
|
||||
<input
|
||||
v-else-if="inputProps(field).type === 'file'"
|
||||
ref="formInput"
|
||||
class="form-input"
|
||||
type="file"
|
||||
:disabled="fieldsToExclude.includes(field.name)"
|
||||
@@ -49,13 +52,14 @@
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
ref="formInput"
|
||||
v-model="localRow[field.name]"
|
||||
class="form-input"
|
||||
:type="inputProps(field).type"
|
||||
:disabled="fieldsToExclude.includes(field.name)"
|
||||
: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 }}
|
||||
</span>
|
||||
<label class="form-checkbox ml-3" :title="$t('word.insert')">
|
||||
@@ -124,8 +128,9 @@ export default {
|
||||
}
|
||||
},
|
||||
props: {
|
||||
connection: Object,
|
||||
tabUid: [String, Number]
|
||||
tabUid: [String, Number],
|
||||
fields: Array,
|
||||
keyUsage: Array
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -145,13 +150,7 @@ export default {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
},
|
||||
foreignKeys () {
|
||||
return this.keyUsage.map(key => key.column);
|
||||
},
|
||||
fields () {
|
||||
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields[0] : [];
|
||||
},
|
||||
keyUsage () {
|
||||
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage[0] : [];
|
||||
return this.keyUsage.map(key => key.field);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -160,6 +159,9 @@ export default {
|
||||
this.nInserts = 1;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
},
|
||||
mounted () {
|
||||
const rowObj = {};
|
||||
|
||||
@@ -194,6 +196,15 @@ export default {
|
||||
}
|
||||
|
||||
this.localRow = { ...rowObj };
|
||||
|
||||
// Auto focus
|
||||
setTimeout(() => {
|
||||
const firstSelectableInput = this.$refs.formInput.find(input => !input.disabled);
|
||||
firstSelectableInput.focus();
|
||||
}, 20);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
@@ -242,7 +253,7 @@ export default {
|
||||
},
|
||||
fieldLength (field) {
|
||||
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
|
||||
return field.numPrecision || field.datePrecision || field.charLength || 0;
|
||||
return field.numLength || field.datePrecision || field.charLength || 0;
|
||||
},
|
||||
inputProps (field) {
|
||||
if ([...TEXT, ...LONG_TEXT].includes(field.type))
|
||||
@@ -294,9 +305,13 @@ export default {
|
||||
|
||||
this.localRow[field] = files[0].path;
|
||||
},
|
||||
|
||||
getKeyUsage (keyName) {
|
||||
return this.keyUsage.find(key => key.column === keyName);
|
||||
return this.keyUsage.find(key => key.field === keyName);
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -111,7 +111,8 @@
|
||||
<h4>{{ appName }}</h4>
|
||||
<p>
|
||||
{{ $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><br>
|
||||
<small>{{ $t('word.author') }}: <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">Fabio Di Stasio</a></small><br>
|
||||
<small>{{ $t('message.madeWithJS') }}</small>
|
||||
</p>
|
||||
</div>
|
||||
@@ -164,6 +165,10 @@ export default {
|
||||
this.localLocale = this.selectedLocale;
|
||||
this.localTimeout = this.notificationsTimeout;
|
||||
this.selectedTab = this.selectedSettingTab;
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
@@ -182,6 +187,11 @@ export default {
|
||||
this.localTimeout = 10;
|
||||
|
||||
this.updateNotificationsTimeout(+this.localTimeout);
|
||||
},
|
||||
onKey (e) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape')
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -14,7 +14,8 @@ monaco.languages.registerCompletionItemProvider('sql', completionItemProvider(mo
|
||||
export default {
|
||||
name: 'QueryEditor',
|
||||
props: {
|
||||
value: String
|
||||
value: String,
|
||||
autoFocus: { type: Boolean, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -40,6 +41,12 @@ export default {
|
||||
const content = this.editor.getValue();
|
||||
this.$emit('update:value', content);
|
||||
});
|
||||
|
||||
if (this.autoFocus) {
|
||||
setTimeout(() => {
|
||||
this.editor.focus();
|
||||
}, 20);
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.editor && this.editor.dispose();
|
||||
|
@@ -10,9 +10,14 @@
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}</span>
|
||||
</div>
|
||||
|
||||
<ModalEditConnection
|
||||
v-if="isEditModal"
|
||||
:connection="contextConnection"
|
||||
@close="hideEditModal"
|
||||
/>
|
||||
<ConfirmModal
|
||||
v-if="isConfirmModal"
|
||||
@confirm="deleteConnection(contextConnection)"
|
||||
@confirm="confirmDeleteConnection"
|
||||
@hide="hideConfirmModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
@@ -33,11 +38,13 @@
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import ModalEditConnection from '@/components/ModalEditConnection';
|
||||
|
||||
export default {
|
||||
name: 'SettingBarContext',
|
||||
components: {
|
||||
BaseContextMenu,
|
||||
ModalEditConnection,
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
@@ -46,7 +53,8 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isConfirmModal: false
|
||||
isConfirmModal: false,
|
||||
isEditModal: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -59,14 +67,28 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
deleteConnection: 'connections/deleteConnection',
|
||||
showEditModal: 'application/showEditConnModal'
|
||||
deleteConnection: 'connections/deleteConnection'
|
||||
}),
|
||||
confirmDeleteConnection () {
|
||||
this.deleteConnection(this.contextConnection);
|
||||
this.closeContext();
|
||||
},
|
||||
showEditModal () {
|
||||
this.isEditModal = true;
|
||||
},
|
||||
hideEditModal () {
|
||||
this.isEditModal = false;
|
||||
this.closeContext();
|
||||
},
|
||||
showConfirmModal () {
|
||||
this.isConfirmModal = true;
|
||||
},
|
||||
hideConfirmModal () {
|
||||
this.isConfirmModal = false;
|
||||
this.closeContext();
|
||||
},
|
||||
closeContext () {
|
||||
this.$emit('close-context');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div id="footer" class="text-light">
|
||||
<div class="footer-left-elements">
|
||||
<ul class="footer-elements">
|
||||
<li class="footer-element">
|
||||
<li class="footer-element" :title="$t('word.version')">
|
||||
<i class="mdi mdi-18px mdi-memory mr-1" />
|
||||
<small>{{ appVersion }}</small>
|
||||
</li>
|
||||
@@ -15,7 +15,7 @@
|
||||
<i class="mdi mdi-18px mdi-coffee mr-1" />
|
||||
<small>{{ $t('word.donate') }}</small>
|
||||
</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" />
|
||||
</li>
|
||||
<li class="footer-element footer-link" @click="showSettingModal('about')">
|
||||
|
@@ -2,7 +2,16 @@
|
||||
<div v-show="isSelected" class="workspace column columns col-gapless">
|
||||
<WorkspaceExploreBar :connection="connection" :is-selected="isSelected" />
|
||||
<div v-if="workspace.connected" class="workspace-tabs column columns col-gapless">
|
||||
<ul ref="tabWrap" class="tab tab-block column col-12">
|
||||
<ul
|
||||
id="tabWrap"
|
||||
ref="tabWrap"
|
||||
class="tab tab-block column col-12"
|
||||
>
|
||||
<li class="tab-item d-none">
|
||||
<a class="tab-link workspace-tools-link">
|
||||
<i class="mdi mdi-24px mdi-tools" />
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="workspace.breadcrumbs.table"
|
||||
class="tab-item"
|
||||
@@ -31,9 +40,10 @@
|
||||
class="tab-item"
|
||||
:class="{'active': selectedTab === tab.uid}"
|
||||
@click="selectTab({uid: workspace.uid, tab: tab.uid})"
|
||||
@mousedown.middle="closeTab(tab.uid)"
|
||||
@mouseup.middle="closeTab(tab.uid)"
|
||||
>
|
||||
<a>
|
||||
<a class="tab-link">
|
||||
<i class="mdi mdi-18px mdi-code-tags mr-1" />
|
||||
<span>
|
||||
Query #{{ tab.index }}
|
||||
<span
|
||||
@@ -55,11 +65,12 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-show="selectedTab === 'prop'" class="column col-12">
|
||||
<p class="px-2">
|
||||
In future releases
|
||||
</p>
|
||||
</div>
|
||||
<WorkspacePropsTab
|
||||
v-show="selectedTab === 'prop'"
|
||||
:is-selected="selectedTab === 'prop'"
|
||||
:connection="connection"
|
||||
:table="workspace.breadcrumbs.table"
|
||||
/>
|
||||
<WorkspaceTableTab
|
||||
v-show="selectedTab === 'data'"
|
||||
:connection="connection"
|
||||
@@ -82,17 +93,24 @@ import Connection from '@/ipc-api/Connection';
|
||||
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
|
||||
import WorkspaceQueryTab from '@/components/WorkspaceQueryTab';
|
||||
import WorkspaceTableTab from '@/components/WorkspaceTableTab';
|
||||
import WorkspacePropsTab from '@/components/WorkspacePropsTab';
|
||||
|
||||
export default {
|
||||
name: 'Workspace',
|
||||
components: {
|
||||
WorkspaceExploreBar,
|
||||
WorkspaceQueryTab,
|
||||
WorkspaceTableTab
|
||||
WorkspaceTableTab,
|
||||
WorkspacePropsTab
|
||||
},
|
||||
props: {
|
||||
connection: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hasWheelEvent: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
@@ -105,7 +123,13 @@ export default {
|
||||
return this.selectedWorkspace === this.connection.uid;
|
||||
},
|
||||
selectedTab () {
|
||||
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) || ['data', 'prop'].includes(this.workspace.selected_tab) ? this.workspace.selected_tab : this.queryTabs[0].uid;
|
||||
if (this.workspace.breadcrumbs.table === null && ['data', 'prop'].includes(this.workspace.selected_tab))
|
||||
return this.queryTabs[0].uid;
|
||||
|
||||
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) ||
|
||||
['data', 'prop'].includes(this.workspace.selected_tab)
|
||||
? this.workspace.selected_tab
|
||||
: this.queryTabs[0].uid;
|
||||
},
|
||||
queryTabs () {
|
||||
return this.workspace.tabs.filter(tab => tab.type === 'query');
|
||||
@@ -136,6 +160,14 @@ export default {
|
||||
}),
|
||||
addTab () {
|
||||
this.newTab(this.connection.uid);
|
||||
|
||||
if (!this.hasWheelEvent) {
|
||||
this.$refs.tabWrap.addEventListener('wheel', e => {
|
||||
if (e.deltaY > 0) this.$refs.tabWrap.scrollLeft += 50;
|
||||
else this.$refs.tabWrap.scrollLeft -= 50;
|
||||
});
|
||||
this.hasWheelEvent = true;
|
||||
}
|
||||
},
|
||||
closeTab (tUid) {
|
||||
if (this.queryTabs.length === 1) return;
|
||||
@@ -202,6 +234,11 @@ export default {
|
||||
&.active a {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.workspace-tools-link {
|
||||
padding-bottom: 0;
|
||||
padding-top: 0.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -39,6 +39,7 @@
|
||||
:database="db"
|
||||
:connection="connection"
|
||||
@show-database-context="openDatabaseContext"
|
||||
@show-table-context="openTableContext"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,11 +48,25 @@
|
||||
@close="hideNewDBModal"
|
||||
@reload="refresh"
|
||||
/>
|
||||
<ModalNewTable
|
||||
v-if="isNewTableModal"
|
||||
:workspace="workspace"
|
||||
@close="hideCreateTableModal"
|
||||
@open-create-table-editor="openCreateTableEditor"
|
||||
/>
|
||||
<DatabaseContext
|
||||
v-if="isDatabaseContext"
|
||||
:selected-database="selectedDatabase"
|
||||
:context-event="databaseContextEvent"
|
||||
@close-context="closeDatabaseContext"
|
||||
@show-create-table-modal="showCreateTableModal"
|
||||
@reload="refresh"
|
||||
/>
|
||||
<TableContext
|
||||
v-if="isTableContext"
|
||||
:selected-table="selectedTable"
|
||||
:context-event="tableContextEvent"
|
||||
@close-context="closeTableContext"
|
||||
@reload="refresh"
|
||||
/>
|
||||
</div>
|
||||
@@ -60,10 +75,13 @@
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import _ from 'lodash';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
|
||||
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase';
|
||||
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext';
|
||||
import TableContext from '@/components/WorkspaceExploreBarTableContext';
|
||||
import ModalNewDatabase from '@/components/ModalNewDatabase';
|
||||
import ModalNewTable from '@/components/ModalNewTable';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceExploreBar',
|
||||
@@ -71,7 +89,9 @@ export default {
|
||||
WorkspaceConnectPanel,
|
||||
WorkspaceExploreBarDatabase,
|
||||
DatabaseContext,
|
||||
ModalNewDatabase
|
||||
TableContext,
|
||||
ModalNewDatabase,
|
||||
ModalNewTable
|
||||
},
|
||||
props: {
|
||||
connection: Object,
|
||||
@@ -81,6 +101,7 @@ export default {
|
||||
return {
|
||||
isRefreshing: false,
|
||||
isNewDBModal: false,
|
||||
isNewTableModal: false,
|
||||
localWidth: null,
|
||||
isDatabaseContext: false,
|
||||
isTableContext: false,
|
||||
@@ -128,6 +149,9 @@ export default {
|
||||
...mapActions({
|
||||
disconnectWorkspace: 'workspaces/removeConnected',
|
||||
refreshStructure: 'workspaces/refreshStructure',
|
||||
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
|
||||
selectTab: 'workspaces/selectTab',
|
||||
addNotification: 'notifications/addNotification',
|
||||
changeExplorebarSize: 'settings/changeExplorebarSize'
|
||||
}),
|
||||
async refresh () {
|
||||
@@ -153,15 +177,44 @@ export default {
|
||||
hideNewDBModal () {
|
||||
this.isNewDBModal = false;
|
||||
},
|
||||
showCreateTableModal () {
|
||||
this.closeDatabaseContext();
|
||||
this.isNewTableModal = true;
|
||||
},
|
||||
hideCreateTableModal () {
|
||||
this.isNewTableModal = false;
|
||||
},
|
||||
async openCreateTableEditor (payload) {
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
...payload
|
||||
};
|
||||
|
||||
const { status, response } = await Tables.createTable(params);
|
||||
|
||||
if (status === 'success') {
|
||||
await this.refresh();
|
||||
this.changeBreadcrumbs({ schema: this.selectedDatabase, table: payload.name });
|
||||
this.selectTab({ uid: this.workspace.uid, tab: 'prop' });
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
},
|
||||
openDatabaseContext (payload) {
|
||||
this.isTableContext = false;
|
||||
this.selectedDatabase = payload.database;
|
||||
this.databaseContextEvent = payload.event;
|
||||
this.isDatabaseContext = true;
|
||||
},
|
||||
closeDatabaseContext () {
|
||||
this.isDatabaseContext = false;
|
||||
this.selectedDatabase = '';
|
||||
},
|
||||
openTableContext (payload) {
|
||||
this.selectedTable = payload.table;
|
||||
this.tableContextEvent = payload.event;
|
||||
this.isTableContext = true;
|
||||
},
|
||||
closeTableContext () {
|
||||
this.isTableContext = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<details class="accordion workspace-explorebar-database">
|
||||
<summary
|
||||
class="accordion-header database-name pb-0"
|
||||
class="accordion-header database-name"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name}"
|
||||
@click="changeBreadcrumbs({schema: database.name, table:null})"
|
||||
@click="changeBreadcrumbs({schema: database.name, table: null})"
|
||||
@contextmenu.prevent="showDatabaseContext($event, database.name)"
|
||||
>
|
||||
<i class="icon mdi mdi-18px mdi-chevron-right" />
|
||||
@@ -15,16 +15,19 @@
|
||||
<ul class="menu menu-nav pt-0">
|
||||
<li
|
||||
v-for="table of database.tables"
|
||||
:key="table.TABLE_NAME"
|
||||
:key="table.name"
|
||||
class="menu-item"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.TABLE_NAME}"
|
||||
@click="changeBreadcrumbs({schema: database.name, table: table.TABLE_NAME})"
|
||||
@contextmenu.prevent="showTableContext($event, table.TABLE_NAME)"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.name}"
|
||||
@click="setBreadcrumbs({schema: database.name, table: table.name})"
|
||||
@contextmenu.prevent="showTableContext($event, table.name)"
|
||||
>
|
||||
<a class="table-name">
|
||||
<i class="table-icon mdi mdi-18px mdi-table mr-1" />
|
||||
<span>{{ table.TABLE_NAME }}</span>
|
||||
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
|
||||
<span>{{ table.name }}</span>
|
||||
</a>
|
||||
<div class="table-size tooltip tooltip-left mr-1" :data-tooltip="formatBytes(table.size)">
|
||||
<div class="pie" :style="piePercentage(table.size)" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -34,6 +37,7 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { formatBytes } from 'common/libs/formatBytes';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceExploreBarDatabase',
|
||||
@@ -47,57 +51,121 @@ export default {
|
||||
}),
|
||||
breadcrumbs () {
|
||||
return this.getWorkspace(this.connection.uid).breadcrumbs;
|
||||
},
|
||||
maxSize () {
|
||||
return this.database.tables.reduce((acc, curr) => {
|
||||
if (curr.size > acc) acc = curr.size;
|
||||
return acc;
|
||||
}, 0);
|
||||
},
|
||||
totalSize () {
|
||||
return this.database.tables.reduce((acc, curr) => acc + curr.size, 0);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
|
||||
}),
|
||||
formatBytes,
|
||||
showDatabaseContext (event, database) {
|
||||
this.$emit('show-database-context', { event, database });
|
||||
},
|
||||
showTableContext (table) {
|
||||
this.$emit('show-table-context', table);
|
||||
showTableContext (event, table) {
|
||||
this.$emit('show-table-context', { event, table });
|
||||
},
|
||||
piePercentage (val) {
|
||||
const perc = val / this.maxSize * 100;
|
||||
return { background: `conic-gradient(lime ${perc}%, white 0)` };
|
||||
},
|
||||
setBreadcrumbs (payload) {
|
||||
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
|
||||
this.changeBreadcrumbs(payload);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.workspace-explorebar-database {
|
||||
.database-name,
|
||||
a.table-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.7rem;
|
||||
.workspace-explorebar-database {
|
||||
.database-name,
|
||||
a.table-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem 1rem 0.1rem 0.1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.7rem;
|
||||
|
||||
> span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $body-font-color;
|
||||
background: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.database-icon,
|
||||
.table-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
> span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.database-tables {
|
||||
margin-left: 1.2rem;
|
||||
.database-icon,
|
||||
.table-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.database-name {
|
||||
&:hover {
|
||||
color: $body-font-color;
|
||||
background: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
a.table-name {
|
||||
&:hover {
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
line-height: 1.2;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: $body-font-color;
|
||||
background: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.database-tables {
|
||||
margin-left: 1.2rem;
|
||||
}
|
||||
|
||||
.table-size {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&::after {
|
||||
font-weight: 400;
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.pie {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -3,15 +3,20 @@
|
||||
:context-event="contextEvent"
|
||||
@close-context="closeContext"
|
||||
>
|
||||
<!-- <div class="context-element">
|
||||
<div class="context-element">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span>
|
||||
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
|
||||
</div> -->
|
||||
<div class="context-submenu">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-element" @click="showEditModal">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
|
||||
</div>
|
||||
<div class="context-element" @click="showDeleteModal">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}</span>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
@@ -64,15 +69,21 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected'
|
||||
})
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace'
|
||||
}),
|
||||
workspace () {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
deleteConnection: 'connections/deleteConnection',
|
||||
showEditModal: 'application/showEditConnModal',
|
||||
addNotification: 'notifications/addNotification'
|
||||
addNotification: 'notifications/addNotification',
|
||||
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
|
||||
}),
|
||||
showCreateTableModal () {
|
||||
this.$emit('show-create-table-modal');
|
||||
},
|
||||
showDeleteModal () {
|
||||
this.isDeleteModal = true;
|
||||
},
|
||||
@@ -97,6 +108,9 @@ export default {
|
||||
});
|
||||
|
||||
if (status === 'success') {
|
||||
if (this.selectedDatabase === this.workspace.breadcrumbs.schema)
|
||||
this.changeBreadcrumbs({ schema: null });
|
||||
|
||||
this.closeContext();
|
||||
this.$emit('reload');
|
||||
}
|
||||
|
146
src/renderer/components/WorkspaceExploreBarTableContext.vue
Normal file
146
src/renderer/components/WorkspaceExploreBarTableContext.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<BaseContextMenu
|
||||
:context-event="contextEvent"
|
||||
@close-context="closeContext"
|
||||
>
|
||||
<div 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>
|
||||
</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="isEmptyModal"
|
||||
@confirm="emptyTable"
|
||||
@hide="hideEmptyModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-table-off mr-1" /> {{ $t('message.emptyTable') }}
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<div class="mb-2">
|
||||
{{ $t('message.emptyCorfirm') }} "<b>{{ selectedTable }}</b>"?
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
<ConfirmModal
|
||||
v-if="isDeleteModal"
|
||||
@confirm="deleteTable"
|
||||
@hide="hideDeleteModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-table-remove mr-1" /> {{ $t('message.deleteTable') }}
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<div class="mb-2">
|
||||
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedTable }}</b>"?
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceExploreBarTableContext',
|
||||
components: {
|
||||
BaseContextMenu,
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
contextEvent: MouseEvent,
|
||||
selectedTable: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isDeleteModal: false,
|
||||
isEmptyModal: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace'
|
||||
}),
|
||||
workspace () {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification',
|
||||
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
|
||||
}),
|
||||
showCreateTableModal () {
|
||||
this.$emit('show-create-table-modal');
|
||||
},
|
||||
showDeleteModal () {
|
||||
this.isDeleteModal = true;
|
||||
},
|
||||
hideDeleteModal () {
|
||||
this.isDeleteModal = false;
|
||||
},
|
||||
showEmptyModal () {
|
||||
this.isEmptyModal = true;
|
||||
},
|
||||
hideEmptyModal () {
|
||||
this.isEmptyModal = false;
|
||||
},
|
||||
closeContext () {
|
||||
this.$emit('close-context');
|
||||
},
|
||||
async emptyTable () {
|
||||
try {
|
||||
const { status, response } = await Tables.truncateTable({
|
||||
uid: this.selectedWorkspace,
|
||||
table: this.selectedTable
|
||||
});
|
||||
|
||||
if (status === 'success') {
|
||||
if (this.selectedTable === this.workspace.breadcrumbs.table)
|
||||
this.changeBreadcrumbs({ table: null });
|
||||
|
||||
this.closeContext();
|
||||
this.$emit('reload');
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
},
|
||||
async deleteTable () {
|
||||
try {
|
||||
const { status, response } = await Tables.dropTable({
|
||||
uid: this.selectedWorkspace,
|
||||
table: this.selectedTable
|
||||
});
|
||||
|
||||
if (status === 'success') {
|
||||
if (this.selectedTable === this.workspace.breadcrumbs.table)
|
||||
this.changeBreadcrumbs({ table: null });
|
||||
|
||||
this.closeContext();
|
||||
this.$emit('reload');
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
416
src/renderer/components/WorkspacePropsForeignModal.vue
Normal file
416
src/renderer/components/WorkspacePropsForeignModal.vue
Normal 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>
|
284
src/renderer/components/WorkspacePropsIndexesModal.vue
Normal file
284
src/renderer/components/WorkspacePropsIndexesModal.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<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-key mdi-rotate-45 mr-1" /> {{ $t('word.indexes') }} "{{ 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="addIndex">
|
||||
<span>{{ $t('word.add') }}</span>
|
||||
<i class="mdi mdi-24px mdi-key-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="index in indexesProxy"
|
||||
:key="index._id"
|
||||
class="tile tile-centered c-hand mb-1 p-1"
|
||||
:class="{'selected-index': selectedIndexID === index._id}"
|
||||
@click="selectIndex($event, index._id)"
|
||||
>
|
||||
<div class="tile-icon">
|
||||
<div>
|
||||
<i class="mdi mdi-key mdi-24px column-key" :class="`key-${index.type}`" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile-content">
|
||||
<div class="tile-title">
|
||||
{{ index.name }}
|
||||
</div>
|
||||
<small class="tile-subtitle text-gray">{{ index.type }} · {{ index.fields.length }} {{ $tc('word.field', index.fields.length) }}</small>
|
||||
</div>
|
||||
<div class="tile-action">
|
||||
<button
|
||||
class="btn btn-link remove-field p-0 mr-2"
|
||||
:title="$t('word.delete')"
|
||||
@click.prevent="removeIndex(index._id)"
|
||||
>
|
||||
<i class="mdi mdi-close" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column col-7 pl-2 editor-col">
|
||||
<form
|
||||
v-if="selectedIndexObj"
|
||||
: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="selectedIndexObj.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="selectedIndexObj.type" class="form-select">
|
||||
<option
|
||||
v-for="index in indexTypes"
|
||||
:key="index"
|
||||
:value="index"
|
||||
:disabled="index === 'PRIMARY' && hasPrimary"
|
||||
>
|
||||
{{ index }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-3">
|
||||
{{ $tc('word.field', fields.length) }}
|
||||
</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="selectedIndexObj.fields.some(f => f === field.name)">
|
||||
<i class="form-icon" /> {{ field.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'WorkspacePropsIndexesModal',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
localIndexes: Array,
|
||||
table: String,
|
||||
fields: Array,
|
||||
workspace: Object,
|
||||
indexTypes: Array
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
indexesProxy: [],
|
||||
isOptionsChanging: false,
|
||||
selectedIndexID: '',
|
||||
modalInnerHeight: 400
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedIndexObj () {
|
||||
return this.indexesProxy.find(index => index._id === this.selectedIndexID);
|
||||
},
|
||||
isChanged () {
|
||||
return JSON.stringify(this.localIndexes) !== JSON.stringify(this.indexesProxy);
|
||||
},
|
||||
hasPrimary () {
|
||||
return this.indexesProxy.some(index => index.type === 'PRIMARY');
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes));
|
||||
|
||||
if (this.indexesProxy.length)
|
||||
this.resetSelectedID();
|
||||
|
||||
this.getModalInnerHeight();
|
||||
window.addEventListener('resize', this.getModalInnerHeight);
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('resize', this.getModalInnerHeight);
|
||||
},
|
||||
methods: {
|
||||
confirmIndexesChange () {
|
||||
this.$emit('indexes-update', this.indexesProxy);
|
||||
},
|
||||
selectIndex (event, id) {
|
||||
if (this.selectedIndexID !== id && !event.target.classList.contains('remove-field'))
|
||||
this.selectedIndexID = id;
|
||||
},
|
||||
getModalInnerHeight () {
|
||||
const modalBody = document.querySelector('.modal-body');
|
||||
if (modalBody)
|
||||
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
|
||||
},
|
||||
addIndex () {
|
||||
this.indexesProxy = [...this.indexesProxy, {
|
||||
_id: uidGen(),
|
||||
name: 'NEW_INDEX',
|
||||
fields: [],
|
||||
type: 'INDEX',
|
||||
comment: '',
|
||||
indexType: 'BTREE',
|
||||
indexComment: '',
|
||||
cardinality: 0
|
||||
}];
|
||||
|
||||
if (this.indexesProxy.length === 1)
|
||||
this.resetSelectedID();
|
||||
|
||||
setTimeout(() => {
|
||||
this.$refs.indexesPanel.scrollTop = this.$refs.indexesPanel.scrollHeight + 60;
|
||||
}, 20);
|
||||
},
|
||||
removeIndex (id) {
|
||||
this.indexesProxy = this.indexesProxy.filter(index => index._id !== id);
|
||||
|
||||
if (this.selectedIndexID === id && this.indexesProxy.length)
|
||||
this.resetSelectedID();
|
||||
},
|
||||
clearChanges () {
|
||||
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes));
|
||||
if (!this.indexesProxy.some(index => index._id === this.selectedIndexID))
|
||||
this.resetSelectedID();
|
||||
},
|
||||
toggleField (field) {
|
||||
this.indexesProxy = this.indexesProxy.map(index => {
|
||||
if (index._id === this.selectedIndexID) {
|
||||
if (index.fields.includes(field))
|
||||
index.fields = index.fields.filter(f => f !== field);
|
||||
else
|
||||
index.fields.push(field);
|
||||
}
|
||||
return index;
|
||||
});
|
||||
},
|
||||
resetSelectedID () {
|
||||
this.selectedIndexID = this.indexesProxy.length ? this.indexesProxy[0]._id : '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tile {
|
||||
border-radius: 2px;
|
||||
opacity: 0.5;
|
||||
transition: background 0.2s;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.tile-action {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $bg-color-light;
|
||||
|
||||
.tile-action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected-index {
|
||||
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>
|
130
src/renderer/components/WorkspacePropsOptionsModal.vue
Normal file
130
src/renderer/components/WorkspacePropsOptionsModal.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<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') }} "{{ table }}"
|
||||
</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.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('word.autoIncrement') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="optionsProxy.autoIncrement"
|
||||
class="form-input"
|
||||
type="number"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.collation') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="optionsProxy.collation" class="form-select">
|
||||
<option
|
||||
v-for="collation in workspace.collations"
|
||||
:key="collation.id"
|
||||
:value="collation.collation"
|
||||
>
|
||||
{{ collation.collation }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('word.engine') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<select v-model="optionsProxy.engine" class="form-select">
|
||||
<option
|
||||
v-for="engine in workspace.engines"
|
||||
:key="engine.name"
|
||||
:value="engine.name"
|
||||
>
|
||||
{{ engine.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'WorkspacePropsOptionsModal',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
localOptions: Object,
|
||||
table: String,
|
||||
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>
|
496
src/renderer/components/WorkspacePropsTab.vue
Normal file
496
src/renderer/components/WorkspacePropsTab.vue
Normal file
@@ -0,0 +1,496 @@
|
||||
<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"
|
||||
:title="$t('message.addNewField')"
|
||||
@click="addField"
|
||||
>
|
||||
<span>{{ $t('word.add') }}</span>
|
||||
<i class="mdi mdi-24px mdi-playlist-plus ml-1" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-dark btn-sm"
|
||||
:title="$t('message.manageIndexes')"
|
||||
@click="showIntdexesModal"
|
||||
>
|
||||
<span>{{ $t('word.indexes') }}</span>
|
||||
<i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" />
|
||||
</button>
|
||||
<button class="btn btn-dark btn-sm" @click="showForeignModal">
|
||||
<span>{{ $t('word.foreignKeys') }}</span>
|
||||
<i class="mdi mdi-24px mdi-key-link 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">
|
||||
<WorkspacePropsTable
|
||||
v-if="localFields"
|
||||
ref="indexTable"
|
||||
:fields="localFields"
|
||||
:indexes="localIndexes"
|
||||
:foreigns="localKeyUsage"
|
||||
:tab-uid="tabUid"
|
||||
:conn-uid="connection.uid"
|
||||
:index-types="workspace.indexTypes"
|
||||
:table="table"
|
||||
:schema="schema"
|
||||
mode="table"
|
||||
@remove-field="removeField"
|
||||
@add-new-index="addNewIndex"
|
||||
@add-to-index="addToIndex"
|
||||
/>
|
||||
</div>
|
||||
<WorkspacePropsOptionsModal
|
||||
v-if="isOptionsModal"
|
||||
:local-options="localOptions"
|
||||
:table="table"
|
||||
:workspace="workspace"
|
||||
@hide="hideOptionsModal"
|
||||
@options-update="optionsUpdate"
|
||||
/>
|
||||
<WorkspacePropsIndexesModal
|
||||
v-if="isIndexesModal"
|
||||
:local-indexes="localIndexes"
|
||||
:table="table"
|
||||
:fields="localFields"
|
||||
:index-types="workspace.indexTypes"
|
||||
:workspace="workspace"
|
||||
@hide="hideIndexesModal"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import WorkspacePropsTable from '@/components/WorkspacePropsTable';
|
||||
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
|
||||
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
|
||||
import WorkspacePropsForeignModal from '@/components/WorkspacePropsForeignModal';
|
||||
|
||||
export default {
|
||||
name: 'WorkspacePropsTab',
|
||||
components: {
|
||||
WorkspacePropsTable,
|
||||
WorkspacePropsOptionsModal,
|
||||
WorkspacePropsIndexesModal,
|
||||
WorkspacePropsForeignModal
|
||||
},
|
||||
props: {
|
||||
connection: Object,
|
||||
table: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tabUid: 'prop',
|
||||
isQuering: false,
|
||||
isSaving: false,
|
||||
isOptionsModal: false,
|
||||
isIndexesModal: false,
|
||||
isForeignModal: false,
|
||||
isOptionsChanging: false,
|
||||
originalFields: [],
|
||||
localFields: [],
|
||||
originalKeyUsage: [],
|
||||
localKeyUsage: [],
|
||||
originalIndexes: [],
|
||||
localIndexes: [],
|
||||
localOptions: {},
|
||||
lastTable: null,
|
||||
newFieldsCounter: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getWorkspace: 'workspaces/getWorkspace',
|
||||
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
||||
}),
|
||||
workspace () {
|
||||
return this.getWorkspace(this.connection.uid);
|
||||
},
|
||||
tableOptions () {
|
||||
const db = this.workspace.structure.find(db => db.name === this.schema);
|
||||
return db && this.table ? db.tables.find(table => table.name === this.table) : {};
|
||||
},
|
||||
defaultEngine () {
|
||||
return this.getDatabaseVariable(this.connection.uid, 'default_storage_engine').value || '';
|
||||
},
|
||||
isSelected () {
|
||||
return this.workspace.selected_tab === 'prop';
|
||||
},
|
||||
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 () {
|
||||
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) ||
|
||||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
|
||||
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) ||
|
||||
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
table () {
|
||||
if (this.isSelected) {
|
||||
this.getFieldsData();
|
||||
this.lastTable = this.table;
|
||||
}
|
||||
},
|
||||
isSelected (val) {
|
||||
if (val && this.lastTable !== this.table) {
|
||||
this.getFieldsData();
|
||||
this.lastTable = this.table;
|
||||
}
|
||||
},
|
||||
isChanged (val) {
|
||||
if (this.isSelected && this.lastTable === this.table && this.table !== null)
|
||||
this.setUnsavedChanges(val);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification',
|
||||
refreshStructure: 'workspaces/refreshStructure',
|
||||
setUnsavedChanges: 'workspaces/setUnsavedChanges'
|
||||
}),
|
||||
async getFieldsData () {
|
||||
if (!this.table) return;
|
||||
this.newFieldsCounter = 0;
|
||||
this.isQuering = true;
|
||||
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
|
||||
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
schema: this.schema,
|
||||
table: this.workspace.breadcrumbs.table
|
||||
};
|
||||
|
||||
try { // Columns data
|
||||
const { status, response } = await Tables.getTableColumns(params);
|
||||
if (status === 'success') {
|
||||
this.originalFields = response.map(field => {
|
||||
return { ...field, _id: uidGen() };
|
||||
});
|
||||
this.localFields = JSON.parse(JSON.stringify(this.originalFields));
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
try { // Indexes
|
||||
const { status, response } = await Tables.getTableIndexes(params);
|
||||
|
||||
if (status === 'success') {
|
||||
const indexesObj = response.reduce((acc, curr) => {
|
||||
acc[curr.name] = acc[curr.name] || [];
|
||||
acc[curr.name].push(curr);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.originalIndexes = Object.keys(indexesObj).map(index => {
|
||||
return {
|
||||
_id: uidGen(),
|
||||
name: index,
|
||||
fields: indexesObj[index].map(field => field.column),
|
||||
type: indexesObj[index][0].type,
|
||||
comment: indexesObj[index][0].comment,
|
||||
indexType: indexesObj[index][0].indexType,
|
||||
indexComment: indexesObj[index][0].indexComment,
|
||||
cardinality: indexesObj[index][0].cardinality
|
||||
};
|
||||
});
|
||||
|
||||
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes));
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
try { // Key usage (foreign keys)
|
||||
const { status, response } = await Tables.getKeyUsage(params);
|
||||
|
||||
if (status === 'success') {
|
||||
this.originalKeyUsage = response.map(foreign => {
|
||||
return {
|
||||
_id: uidGen(),
|
||||
...foreign
|
||||
};
|
||||
});
|
||||
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.isQuering = false;
|
||||
},
|
||||
async saveChanges () {
|
||||
if (this.isSaving) return;
|
||||
this.isSaving = true;
|
||||
|
||||
// FIELDS
|
||||
const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._id], []);
|
||||
const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._id], []);
|
||||
|
||||
// Fields Additions
|
||||
const additions = this.localFields.filter((field, i) => !originalIDs.includes(field._id)).map(field => {
|
||||
const lI = this.localFields.findIndex(localField => localField._id === field._id);
|
||||
const after = lI > 0 ? this.localFields[lI - 1].name : false;
|
||||
return { ...field, after };
|
||||
});
|
||||
|
||||
// Fields Deletions
|
||||
const deletions = this.originalFields.filter(field => !localIDs.includes(field._id));
|
||||
|
||||
// Fields Changes
|
||||
const changes = [];
|
||||
this.originalFields.forEach((originalField, oI) => {
|
||||
const lI = this.localFields.findIndex(localField => localField._id === originalField._id);
|
||||
const originalSibling = oI > 0 ? this.originalFields[oI - 1]._id : false;
|
||||
const localSibling = lI > 0 ? this.localFields[lI - 1]._id : false;
|
||||
const after = lI > 0 ? this.localFields[lI - 1].name : false;
|
||||
const orgName = originalField.name;
|
||||
|
||||
if (JSON.stringify(originalField) !== JSON.stringify(this.localFields[lI]) || originalSibling !== localSibling)
|
||||
if (this.localFields[lI]) changes.push({ ...this.localFields[lI], after, orgName });
|
||||
});
|
||||
|
||||
// OPTIONS
|
||||
const options = Object.keys(this.localOptions).reduce((acc, option) => {
|
||||
if (this.localOptions[option] !== this.tableOptions[option])
|
||||
acc[option] = this.localOptions[option];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// INDEXES
|
||||
const indexChanges = {
|
||||
additions: [],
|
||||
changes: [],
|
||||
deletions: []
|
||||
};
|
||||
const originalIndexIDs = this.originalIndexes.reduce((acc, curr) => [...acc, curr._id], []);
|
||||
const localIndexIDs = this.localIndexes.reduce((acc, curr) => [...acc, curr._id], []);
|
||||
|
||||
// Index Additions
|
||||
indexChanges.additions = this.localIndexes.filter(index => !originalIndexIDs.includes(index._id));
|
||||
|
||||
// Index Changes
|
||||
this.originalIndexes.forEach(originalIndex => {
|
||||
const lI = this.localIndexes.findIndex(localIndex => localIndex._id === originalIndex._id);
|
||||
if (JSON.stringify(originalIndex) !== JSON.stringify(this.localIndexes[lI])) {
|
||||
if (this.localIndexes[lI]) {
|
||||
indexChanges.changes.push({
|
||||
...this.localIndexes[lI],
|
||||
oldName: originalIndex.name,
|
||||
oldType: originalIndex.type
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Index Deletions
|
||||
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 = {
|
||||
uid: this.connection.uid,
|
||||
schema: this.schema,
|
||||
table: this.workspace.breadcrumbs.table,
|
||||
additions,
|
||||
changes,
|
||||
deletions,
|
||||
indexChanges,
|
||||
foreignChanges,
|
||||
options
|
||||
};
|
||||
|
||||
try {
|
||||
const { status, response } = await Tables.alterTable(params);
|
||||
|
||||
if (status === 'success') {
|
||||
await this.refreshStructure(this.connection.uid);
|
||||
this.getFieldsData();
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.isSaving = false;
|
||||
this.newFieldsCounter = 0;
|
||||
},
|
||||
clearChanges () {
|
||||
this.localFields = JSON.parse(JSON.stringify(this.originalFields));
|
||||
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes));
|
||||
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
|
||||
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
|
||||
this.newFieldsCounter = 0;
|
||||
},
|
||||
addField () {
|
||||
this.localFields.push({
|
||||
_id: uidGen(),
|
||||
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
|
||||
key: '',
|
||||
type: 'int',
|
||||
schema: this.schema,
|
||||
table: this.table,
|
||||
numPrecision: null,
|
||||
numLength: null,
|
||||
datePrecision: null,
|
||||
charLength: null,
|
||||
nullable: false,
|
||||
unsigned: false,
|
||||
zerofill: false,
|
||||
order: this.localFields.length + 1,
|
||||
default: null,
|
||||
charset: null,
|
||||
collation: null,
|
||||
autoIncrement: false,
|
||||
onUpdate: '',
|
||||
comment: ''
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const scrollable = this.$refs.indexTable.$refs.tableWrapper;
|
||||
scrollable.scrollTop = scrollable.scrollHeight + 30;
|
||||
}, 20);
|
||||
},
|
||||
removeField (uid) {
|
||||
this.localFields = this.localFields.filter(field => field._id !== uid);
|
||||
},
|
||||
addNewIndex (payload) {
|
||||
this.localIndexes = [...this.localIndexes, {
|
||||
_id: uidGen(),
|
||||
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
|
||||
fields: [payload.field],
|
||||
type: payload.index,
|
||||
comment: '',
|
||||
indexType: 'BTREE',
|
||||
indexComment: '',
|
||||
cardinality: 0
|
||||
}];
|
||||
},
|
||||
addToIndex (payload) {
|
||||
this.localIndexes = this.localIndexes.map(index => {
|
||||
if (index._id === payload.index) index.fields.push(payload.field);
|
||||
return index;
|
||||
});
|
||||
},
|
||||
showOptionsModal () {
|
||||
this.isOptionsModal = true;
|
||||
},
|
||||
hideOptionsModal () {
|
||||
this.isOptionsModal = false;
|
||||
},
|
||||
optionsUpdate (options) {
|
||||
this.localOptions = options;
|
||||
},
|
||||
showIntdexesModal () {
|
||||
this.isIndexesModal = true;
|
||||
},
|
||||
hideIndexesModal () {
|
||||
this.isIndexesModal = false;
|
||||
},
|
||||
indexesUpdate (indexes) {
|
||||
this.localIndexes = indexes;
|
||||
},
|
||||
showForeignModal () {
|
||||
this.isForeignModal = true;
|
||||
},
|
||||
hideForeignModal () {
|
||||
this.isForeignModal = false;
|
||||
},
|
||||
foreignsUpdate (foreigns) {
|
||||
this.localKeyUsage = foreigns;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
247
src/renderer/components/WorkspacePropsTable.vue
Normal file
247
src/renderer/components/WorkspacePropsTable.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div
|
||||
ref="tableWrapper"
|
||||
class="vscroll"
|
||||
:style="{'height': resultsSize+'px'}"
|
||||
>
|
||||
<TableContext
|
||||
v-if="isContext"
|
||||
:context-event="contextEvent"
|
||||
:selected-field="selectedField"
|
||||
:index-types="indexTypes"
|
||||
:indexes="indexes"
|
||||
@delete-selected="removeField"
|
||||
@close-context="isContext = false"
|
||||
@add-new-index="$emit('add-new-index', $event)"
|
||||
@add-to-index="$emit('add-to-index', $event)"
|
||||
/>
|
||||
<div ref="propTable" class="table table-hover">
|
||||
<div class="thead">
|
||||
<div class="tr">
|
||||
<div class="th">
|
||||
<div class="text-right">
|
||||
{{ $t('word.order') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="table-column-title">
|
||||
{{ $tc('word.key', 2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="column-resizable min-100">
|
||||
<div class="table-column-title">
|
||||
{{ $t('word.name') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="column-resizable min-100">
|
||||
<div class="table-column-title">
|
||||
{{ $t('word.type') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="column-resizable">
|
||||
<div class="table-column-title">
|
||||
{{ $t('word.length') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="column-resizable">
|
||||
<div class="table-column-title">
|
||||
{{ $t('word.unsigned') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="column-resizable">
|
||||
<div class="table-column-title">
|
||||
{{ $t('message.allowNull') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="column-resizable">
|
||||
<div class="table-column-title">
|
||||
{{ $t('message.zeroFill') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="column-resizable">
|
||||
<div class="table-column-title">
|
||||
{{ $t('word.default') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="column-resizable">
|
||||
<div class="table-column-title">
|
||||
{{ $t('word.comment') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="th">
|
||||
<div class="column-resizable min-100">
|
||||
<div class="table-column-title">
|
||||
{{ $t('word.collation') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
ref="resultTable"
|
||||
:list="fields"
|
||||
class="tbody"
|
||||
handle=".row-draggable"
|
||||
>
|
||||
<TableRow
|
||||
v-for="row in fields"
|
||||
:key="row._id"
|
||||
:row="row"
|
||||
:indexes="getIndexes(row.name)"
|
||||
:foreigns="getForeigns(row.name)"
|
||||
:data-types="dataTypes"
|
||||
@contextmenu="contextMenu"
|
||||
/>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import draggable from 'vuedraggable';
|
||||
import TableRow from '@/components/WorkspacePropsTableRow';
|
||||
import TableContext from '@/components/WorkspacePropsTableContext';
|
||||
|
||||
export default {
|
||||
name: 'WorkspacePropsTable',
|
||||
components: {
|
||||
TableRow,
|
||||
TableContext,
|
||||
draggable
|
||||
},
|
||||
props: {
|
||||
fields: Array,
|
||||
indexes: Array,
|
||||
foreigns: Array,
|
||||
indexTypes: Array,
|
||||
tabUid: [String, Number],
|
||||
connUid: String,
|
||||
table: String,
|
||||
schema: String,
|
||||
mode: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resultsSize: 1000,
|
||||
isContext: false,
|
||||
contextEvent: null,
|
||||
selectedField: null,
|
||||
scrollElement: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getWorkspaceTab: 'workspaces/getWorkspaceTab',
|
||||
getWorkspace: 'workspaces/getWorkspace'
|
||||
}),
|
||||
workspaceSchema () {
|
||||
return this.getWorkspace(this.connUid).breadcrumbs.schema;
|
||||
},
|
||||
dataTypes () {
|
||||
return this.getWorkspace(this.connUid).dataTypes;
|
||||
},
|
||||
primaryField () {
|
||||
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
|
||||
},
|
||||
tabProperties () {
|
||||
return this.getWorkspaceTab(this.tabUid);
|
||||
},
|
||||
fieldsLength () {
|
||||
return this.fields.length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fieldsLength () {
|
||||
this.refreshScroller();
|
||||
}
|
||||
},
|
||||
updated () {
|
||||
if (this.$refs.propTable)
|
||||
this.refreshScroller();
|
||||
|
||||
if (this.$refs.tableWrapper)
|
||||
this.scrollElement = this.$refs.tableWrapper;
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('resize', this.resizeResults);
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('resize', this.resizeResults);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
resizeResults () {
|
||||
if (this.$refs.resultTable) {
|
||||
const el = this.$refs.tableWrapper;
|
||||
|
||||
if (el) {
|
||||
const footer = document.getElementById('footer');
|
||||
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
|
||||
this.resultsSize = size;
|
||||
}
|
||||
}
|
||||
},
|
||||
refreshScroller () {
|
||||
this.resizeResults();
|
||||
},
|
||||
contextMenu (event, uid) {
|
||||
this.selectedField = this.fields.find(field => field._id === uid);
|
||||
this.contextEvent = event;
|
||||
this.isContext = true;
|
||||
},
|
||||
removeField () {
|
||||
this.$emit('remove-field', this.selectedField._id);
|
||||
},
|
||||
getIndexes (field) {
|
||||
return this.indexes.reduce((acc, curr) => {
|
||||
acc.push(...curr.fields.map(f => ({ name: f, type: curr.type })));
|
||||
return acc;
|
||||
}, []).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;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.column-resizable {
|
||||
&:hover,
|
||||
&:active {
|
||||
resize: horizontal;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.vscroll {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.min-100 {
|
||||
min-width: 100px !important;
|
||||
}
|
||||
</style>
|
87
src/renderer/components/WorkspacePropsTableContext.vue
Normal file
87
src/renderer/components/WorkspacePropsTableContext.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<BaseContextMenu
|
||||
:context-event="contextEvent"
|
||||
@close-context="closeContext"
|
||||
>
|
||||
<div class="context-element">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-key-plus text-light pr-1" /> {{ $t('message.createNewIndex') }}</span>
|
||||
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
|
||||
<div class="context-submenu">
|
||||
<div
|
||||
v-for="index in indexTypes"
|
||||
:key="index"
|
||||
class="context-element"
|
||||
:class="{'disabled': index === 'PRIMARY' && hasPrimary}"
|
||||
@click="addNewIndex(index)"
|
||||
>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-key column-key pr-1" :class="`key-${index}`" /> {{ index }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="indexes.length" class="context-element">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-key-arrow-right text-light pr-1" /> {{ $t('message.addToIndex') }}</span>
|
||||
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
|
||||
<div class="context-submenu">
|
||||
<div
|
||||
v-for="index in indexes"
|
||||
:key="index.name"
|
||||
class="context-element"
|
||||
:class="{'disabled': index.fields.includes(selectedField.name)}"
|
||||
@click="addToIndex(index._id)"
|
||||
>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-key column-key pr-1" :class="`key-${index.type}`" /> {{ index.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-element" @click="deleteField">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('message.deleteField') }}</span>
|
||||
</div>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceQueryTableContext',
|
||||
components: {
|
||||
BaseContextMenu
|
||||
},
|
||||
props: {
|
||||
contextEvent: MouseEvent,
|
||||
indexes: Array,
|
||||
indexTypes: Array,
|
||||
selectedField: Object
|
||||
},
|
||||
computed: {
|
||||
hasPrimary () {
|
||||
return this.indexes.some(index => index.type === 'PRIMARY');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeContext () {
|
||||
this.$emit('close-context');
|
||||
},
|
||||
deleteField () {
|
||||
this.$emit('delete-selected');
|
||||
this.closeContext();
|
||||
},
|
||||
addNewIndex (index) {
|
||||
this.$emit('add-new-index', { field: this.selectedField.name, index });
|
||||
this.closeContext();
|
||||
},
|
||||
addToIndex (index) {
|
||||
this.$emit('add-to-index', { field: this.selectedField.name, index });
|
||||
this.closeContext();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
553
src/renderer/components/WorkspacePropsTableRow.vue
Normal file
553
src/renderer/components/WorkspacePropsTableRow.vue
Normal file
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)">
|
||||
<div class="td" tabindex="0">
|
||||
<div class="row-draggable">
|
||||
<i class="mdi mdi-drag-horizontal row-draggable-icon" />
|
||||
{{ localRow.order }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="td" tabindex="0">
|
||||
<div class="text-center">
|
||||
<i
|
||||
v-for="(index, i) in indexes"
|
||||
:key="`${index.name}-${i}`"
|
||||
:title="index.type"
|
||||
class="d-inline-block mdi mdi-key column-key c-help"
|
||||
: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 class="td" tabindex="0">
|
||||
<span
|
||||
v-if="!isInlineEditor.name"
|
||||
class="cell-content"
|
||||
@dblclick="editON($event, localRow.name , 'name')"
|
||||
>
|
||||
{{ localRow.name }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
ref="editField"
|
||||
v-model="editingContent"
|
||||
type="text"
|
||||
autofocus
|
||||
class="editable-field px-2"
|
||||
@blur="editOFF"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="td text-uppercase"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
v-if="!isInlineEditor.type"
|
||||
class="cell-content text-left"
|
||||
:class="`type-${lowerCase(localRow.type)}`"
|
||||
@click="editON($event, localRow.type.toUpperCase(), 'type')"
|
||||
>
|
||||
{{ localRow.type }}
|
||||
</span>
|
||||
<select
|
||||
v-else
|
||||
ref="editField"
|
||||
v-model="editingContent"
|
||||
class="form-select editable-field small-select text-uppercase"
|
||||
@blur="editOFF"
|
||||
>
|
||||
<optgroup
|
||||
v-for="group in dataTypes"
|
||||
:key="group.group"
|
||||
:label="group.group"
|
||||
>
|
||||
<option
|
||||
v-for="type in group.types"
|
||||
:key="type.name"
|
||||
:selected="localRow.type.toUpperCase() === type.name"
|
||||
:value="type.name"
|
||||
>
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div class="td type-int" tabindex="0">
|
||||
<template v-if="fieldType.length">
|
||||
<span
|
||||
v-if="!isInlineEditor.length"
|
||||
class="cell-content"
|
||||
@dblclick="editON($event, localLength, 'length')"
|
||||
>
|
||||
{{ localLength }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
ref="editField"
|
||||
v-model="editingContent"
|
||||
type="number"
|
||||
autofocus
|
||||
class="editable-field px-2"
|
||||
@blur="editOFF"
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div class="td" tabindex="0">
|
||||
<label class="form-checkbox">
|
||||
<input
|
||||
v-model="localRow.unsigned"
|
||||
type="checkbox"
|
||||
:disabled="!fieldType.unsigned"
|
||||
>
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="td" tabindex="0">
|
||||
<label class="form-checkbox">
|
||||
<input
|
||||
v-model="localRow.nullable"
|
||||
type="checkbox"
|
||||
:disabled="!isNullable"
|
||||
>
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="td" tabindex="0">
|
||||
<label class="form-checkbox">
|
||||
<input
|
||||
v-model="localRow.zerofill"
|
||||
type="checkbox"
|
||||
:disabled="!fieldType.zerofill"
|
||||
>
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="td" tabindex="0">
|
||||
<span class="cell-content" @dblclick="editON($event, localRow.default, 'default')">
|
||||
{{ fieldDefault }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="td type-varchar" tabindex="0">
|
||||
<span
|
||||
v-if="!isInlineEditor.comment"
|
||||
class="cell-content"
|
||||
@dblclick="editON($event, localRow.comment , 'comment')"
|
||||
>
|
||||
{{ localRow.comment }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
ref="editField"
|
||||
v-model="editingContent"
|
||||
type="text"
|
||||
autofocus
|
||||
class="editable-field px-2"
|
||||
@blur="editOFF"
|
||||
>
|
||||
</div>
|
||||
<div class="td" tabindex="0">
|
||||
<template v-if="fieldType.collation">
|
||||
<span
|
||||
v-if="!isInlineEditor.collation"
|
||||
class="cell-content"
|
||||
@click="editON($event, localRow.collation, 'collation')"
|
||||
>
|
||||
{{ localRow.collation }}
|
||||
</span>
|
||||
<select
|
||||
v-else
|
||||
ref="editField"
|
||||
v-model="editingContent"
|
||||
class="form-select small-select editable-field"
|
||||
@blur="editOFF"
|
||||
>
|
||||
<option
|
||||
v-for="collation in collations"
|
||||
:key="collation.collation"
|
||||
:selected="localRow.collation === collation.collation"
|
||||
:value="collation.collation"
|
||||
>
|
||||
{{ collation.collation }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
v-if="isDefaultModal"
|
||||
:confirm-text="$t('word.confirm')"
|
||||
size="400"
|
||||
@confirm="editOFF"
|
||||
@hide="hideDefaultModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.default') }} "{{ row.name }}"
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<form class="form-horizontal">
|
||||
<div class="mb-2">
|
||||
<label class="form-radio form-inline">
|
||||
<input
|
||||
v-model="defaultValue.type"
|
||||
type="radio"
|
||||
name="default"
|
||||
value="noval"
|
||||
><i class="form-icon" /> No value
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="form-group">
|
||||
<label class="form-radio form-inline col-4">
|
||||
<input
|
||||
v-model="defaultValue.type"
|
||||
value="custom"
|
||||
type="radio"
|
||||
name="default"
|
||||
><i class="form-icon" /> {{ $t('message.customValue') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="defaultValue.custom"
|
||||
:disabled="defaultValue.type !== 'custom'"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-radio form-inline">
|
||||
<input
|
||||
v-model="defaultValue.type"
|
||||
type="radio"
|
||||
name="default"
|
||||
value="null"
|
||||
><i class="form-icon" /> NULL
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-radio form-inline">
|
||||
<input
|
||||
v-model="defaultValue.type"
|
||||
:disabled="!canAutoincrement"
|
||||
type="radio"
|
||||
name="default"
|
||||
value="autoincrement"
|
||||
><i class="form-icon" /> AUTO_INCREMENT
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="form-group">
|
||||
<label class="form-radio form-inline col-4">
|
||||
<input
|
||||
v-model="defaultValue.type"
|
||||
type="radio"
|
||||
name="default"
|
||||
value="expression"
|
||||
><i class="form-icon" /> {{ $t('word.expression') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="defaultValue.expression"
|
||||
:disabled="defaultValue.type !== 'expression'"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label class="form-label col-4">
|
||||
{{ $t('message.onUpdate') }}
|
||||
</label>
|
||||
<div class="column">
|
||||
<input
|
||||
v-model="defaultValue.onUpdate"
|
||||
class="form-input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'WorkspacePropsTableRow',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
row: Object,
|
||||
dataTypes: Array,
|
||||
indexes: Array,
|
||||
foreigns: Array
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localRow: {},
|
||||
isInlineEditor: {},
|
||||
isDefaultModal: false,
|
||||
defaultValue: {
|
||||
type: 'noval',
|
||||
custom: '',
|
||||
expression: '',
|
||||
onUpdate: ''
|
||||
},
|
||||
editingContent: null,
|
||||
originalContent: null,
|
||||
editingField: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace'
|
||||
}),
|
||||
localLength () {
|
||||
return this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || 0;
|
||||
},
|
||||
fieldType () {
|
||||
const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type =>
|
||||
type.name === (this.localRow.type ? this.localRow.type.toUpperCase() : '')
|
||||
);
|
||||
const group = this.dataTypes.filter(group => group.types.some(type =>
|
||||
type.name === (this.localRow.type ? this.localRow.type.toUpperCase() : ''))
|
||||
);
|
||||
|
||||
return fieldType.length ? { ...fieldType[0], group: group[0].group } : {};
|
||||
},
|
||||
fieldDefault () {
|
||||
if (this.localRow.autoIncrement) return 'AUTO_INCREMENT';
|
||||
if (this.localRow.default === 'NULL') return 'NULL';
|
||||
return this.localRow.default;
|
||||
},
|
||||
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: {
|
||||
localRow () {
|
||||
this.initLocalRow();
|
||||
},
|
||||
row () {
|
||||
this.localRow = this.row;
|
||||
},
|
||||
indexes () {
|
||||
if (!this.canAutoincrement)
|
||||
this.localRow.autoIncrement = false;
|
||||
|
||||
if (!this.isNullable)
|
||||
this.localRow.nullable = false;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.localRow = this.row;
|
||||
this.initLocalRow();
|
||||
this.isInlineEditor.length = false;
|
||||
},
|
||||
methods: {
|
||||
keyName (key) {
|
||||
switch (key) {
|
||||
case 'pri':
|
||||
return 'PRIMARY';
|
||||
case 'uni':
|
||||
return 'UNIQUE';
|
||||
case 'mul':
|
||||
return 'INDEX';
|
||||
default:
|
||||
return 'UNKNOWN ' + key;
|
||||
}
|
||||
},
|
||||
lowerCase (val) {
|
||||
if (val)
|
||||
return val.toLowerCase();
|
||||
return val;
|
||||
},
|
||||
initLocalRow () {
|
||||
Object.keys(this.localRow).forEach(key => {
|
||||
this.isInlineEditor[key] = false;
|
||||
});
|
||||
|
||||
this.defaultValue.onUpdate = this.localRow.onUpdate;
|
||||
|
||||
if (this.localRow.autoIncrement)
|
||||
this.defaultValue.type = 'autoincrement';
|
||||
else if (this.localRow.default === null)
|
||||
this.defaultValue.type = 'noval';
|
||||
else if (this.localRow.default === 'NULL')
|
||||
this.defaultValue.type = 'null';
|
||||
else if (this.localRow.default.match(/^'.*'$/g)) {
|
||||
this.defaultValue.type = 'custom';
|
||||
this.defaultValue.custom = this.localRow.default.replace(/(^')|('$)/g, '');
|
||||
}
|
||||
else if (!isNaN(this.localRow.default)) {
|
||||
this.defaultValue.type = 'custom';
|
||||
this.defaultValue.custom = this.localRow.default;
|
||||
}
|
||||
else {
|
||||
this.defaultValue.type = 'expression';
|
||||
this.defaultValue.expression = this.localRow.default;
|
||||
}
|
||||
},
|
||||
updateRow () {
|
||||
this.$emit('input', this.localRow);
|
||||
},
|
||||
editON (event, content, field) {
|
||||
if (field === 'length') {
|
||||
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.editingField = 'numLength';
|
||||
if (['string', 'other'].includes(this.fieldType.group)) this.editingField = 'charLength';
|
||||
if (['time'].includes(this.fieldType.group)) this.editingField = 'datePrecision';
|
||||
}
|
||||
else
|
||||
this.editingField = field;
|
||||
|
||||
this.editingContent = content;
|
||||
this.originalContent = content;
|
||||
|
||||
const obj = { [field]: true };
|
||||
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
|
||||
|
||||
if (field === 'default')
|
||||
this.isDefaultModal = true;
|
||||
else {
|
||||
this.$nextTick(() => { // Focus on input
|
||||
event.target.blur();
|
||||
|
||||
this.$nextTick(() => document.querySelector('.editable-field').focus());
|
||||
});
|
||||
}
|
||||
},
|
||||
editOFF () {
|
||||
this.localRow[this.editingField] = this.editingContent;
|
||||
|
||||
if (this.editingField === 'type' && this.editingContent !== this.originalContent) {
|
||||
this.localRow.numLength = null;
|
||||
this.localRow.charLength = null;
|
||||
this.localRow.datePrecision = null;
|
||||
|
||||
if (this.fieldType.length) {
|
||||
if (['integer', 'float', 'binary', 'spatial'].includes(this.fieldType.group)) this.localRow.numLength = 11;
|
||||
if (['string', 'other'].includes(this.fieldType.group)) this.localRow.charLength = 15;
|
||||
if (['time'].includes(this.fieldType.group)) this.localRow.datePrecision = 0;
|
||||
}
|
||||
|
||||
if (!this.fieldType.collation)
|
||||
this.localRow.collation = null;
|
||||
|
||||
if (!this.fieldType.unsigned)
|
||||
this.localRow.unsigned = false;
|
||||
|
||||
if (!this.fieldType.zerofill)
|
||||
this.localRow.zerofill = false;
|
||||
}
|
||||
|
||||
if (this.editingField === 'default') {
|
||||
switch (this.defaultValue.type) {
|
||||
case 'autoincrement':
|
||||
this.localRow.autoIncrement = true;
|
||||
break;
|
||||
case 'noval':
|
||||
this.localRow.autoIncrement = false;
|
||||
this.localRow.default = null;
|
||||
break;
|
||||
case 'null':
|
||||
this.localRow.autoIncrement = false;
|
||||
this.localRow.default = 'NULL';
|
||||
break;
|
||||
case 'custom':
|
||||
this.localRow.autoIncrement = false;
|
||||
this.localRow.default = `'${this.defaultValue.custom}'`;
|
||||
break;
|
||||
case 'expression':
|
||||
this.localRow.autoIncrement = false;
|
||||
this.localRow.default = this.defaultValue.expression;
|
||||
break;
|
||||
}
|
||||
|
||||
this.localRow.onUpdate = this.defaultValue.onUpdate;
|
||||
}
|
||||
|
||||
Object.keys(this.isInlineEditor).forEach(key => {
|
||||
this.isInlineEditor = { ...this.isInlineEditor, [key]: false };
|
||||
});
|
||||
|
||||
this.editingContent = null;
|
||||
this.originalContent = null;
|
||||
this.editingField = null;
|
||||
},
|
||||
hideDefaultModal () {
|
||||
this.isDefaultModal = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editable-field {
|
||||
margin: 0;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.row-draggable {
|
||||
position: relative;
|
||||
text-align: right;
|
||||
padding-left: 28px;
|
||||
cursor: grab;
|
||||
|
||||
.row-draggable-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-column-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
min-height: auto;
|
||||
|
||||
.form-icon {
|
||||
top: 0.15rem;
|
||||
left: calc(50% - 8px);
|
||||
}
|
||||
}
|
||||
|
||||
.cell-content {
|
||||
display: block;
|
||||
min-height: 0.8rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@@ -1,28 +1,37 @@
|
||||
<template>
|
||||
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
|
||||
<div class="workspace-query-runner column col-12">
|
||||
<QueryEditor v-if="isSelected" :value.sync="query" />
|
||||
<QueryEditor
|
||||
v-if="isSelected"
|
||||
:auto-focus="true"
|
||||
:value.sync="query"
|
||||
/>
|
||||
<div class="workspace-query-runner-footer">
|
||||
<div class="workspace-query-buttons">
|
||||
<button
|
||||
class="btn btn-link btn-sm"
|
||||
class="btn btn-primary btn-sm"
|
||||
:class="{'loading':isQuering}"
|
||||
:disabled="!query"
|
||||
title="F9"
|
||||
@click="runQuery(query)"
|
||||
>
|
||||
<span>{{ $t('word.run') }}</span>
|
||||
<i class="mdi mdi-24px mdi-play text-success" />
|
||||
<i class="mdi mdi-24px mdi-play" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="workspace-query-info">
|
||||
<div v-if="resultsCount !== false">
|
||||
{{ $t('word.results') }}: <b>{{ resultsCount }}</b>
|
||||
<div v-if="resultsCount">
|
||||
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
|
||||
</div>
|
||||
<div v-if="affectedCount !== false">
|
||||
<div v-if="affectedCount">
|
||||
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b>
|
||||
</div>
|
||||
<div v-if="workspace.breadcrumbs.schema">
|
||||
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.schema }}</b>
|
||||
<div
|
||||
v-if="workspace.breadcrumbs.schema"
|
||||
class="d-flex"
|
||||
:title="$t('word.schema')"
|
||||
>
|
||||
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ workspace.breadcrumbs.schema }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +54,6 @@
|
||||
|
||||
<script>
|
||||
import Database from '@/ipc-api/Database';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import QueryEditor from '@/components/QueryEditor';
|
||||
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
@@ -69,8 +77,8 @@ export default {
|
||||
lastQuery: '',
|
||||
isQuering: false,
|
||||
results: [],
|
||||
resultsCount: false,
|
||||
affectedCount: false
|
||||
resultsCount: 0,
|
||||
affectedCount: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -81,31 +89,21 @@ export default {
|
||||
return this.getWorkspace(this.connection.uid);
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification',
|
||||
setTabFields: 'workspaces/setTabFields',
|
||||
setTabKeyUsage: 'workspaces/setTabKeyUsage'
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
getResultParams (index) {
|
||||
const resultsWithRows = this.results.filter(result => result.rows);
|
||||
let cachedTable;
|
||||
|
||||
if (resultsWithRows[index] && resultsWithRows[index].fields && resultsWithRows[index].fields.length) {
|
||||
return resultsWithRows[index].fields.map(field => {
|
||||
if (field.orgTable) cachedTable = field.orgTable;// Needed for some queries on information_schema
|
||||
return {
|
||||
table: field.orgTable || cachedTable,
|
||||
schema: field.db || 'INFORMATION_SCHEMA'
|
||||
};
|
||||
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
async runQuery (query) {
|
||||
if (!query) return;
|
||||
if (!query || this.isQuering) return;
|
||||
this.isQuering = true;
|
||||
this.clearTabData();
|
||||
this.$refs.queryTable.resetSort();
|
||||
|
||||
try { // Query Data
|
||||
const params = {
|
||||
@@ -118,86 +116,8 @@ export default {
|
||||
|
||||
if (status === 'success') {
|
||||
this.results = Array.isArray(response) ? response : [response];
|
||||
|
||||
let selectedFields = [];
|
||||
const fieldsArr = [];
|
||||
const keysArr = [];
|
||||
let qI = 0;// queries index
|
||||
|
||||
for (const result of this.results) { // cycle queries
|
||||
let fI = 0;// fields index
|
||||
|
||||
if (result.rows) { // if is a select
|
||||
const paramsArr = this.getResultParams(qI);
|
||||
|
||||
selectedFields = result.fields.map(field => field.orgName);
|
||||
this.resultsCount += result.rows.length;
|
||||
|
||||
for (const paramObj of paramsArr) {
|
||||
try { // Table data
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
...paramObj
|
||||
};
|
||||
|
||||
const { status, response } = await Tables.getTableColumns(params);
|
||||
|
||||
if (status === 'success') {
|
||||
let fields = response.filter(field => selectedFields.includes(field.name));
|
||||
if (selectedFields.length) {
|
||||
fields = fields.map(field => {
|
||||
return { ...field, alias: result.fields[fI++].name };
|
||||
});
|
||||
}
|
||||
if (!fields.length) {
|
||||
fields = response.map(field => {
|
||||
return { ...field, alias: result.fields[fI++].name };
|
||||
});
|
||||
}
|
||||
|
||||
fieldsArr[qI] = fieldsArr[qI] ? [...fieldsArr[qI], ...fields] : fields;
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
try { // Key usage (foreign keys)
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
...paramObj
|
||||
};
|
||||
|
||||
const { status, response } = await Tables.getKeyUsage(params);
|
||||
if (status === 'success')
|
||||
keysArr[qI] = keysArr[qI] ? [...keysArr[qI], ...response] : response;
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (result.report) { // if is a query without output
|
||||
this.affectedCount += result.report.affectedRows;
|
||||
}
|
||||
|
||||
qI++;
|
||||
}
|
||||
|
||||
this.setTabFields({
|
||||
cUid: this.connection.uid,
|
||||
tUid: this.tabUid,
|
||||
fields: fieldsArr
|
||||
});
|
||||
this.setTabKeyUsage({
|
||||
cUid: this.connection.uid,
|
||||
tUid: this.tabUid,
|
||||
keyUsage: keysArr
|
||||
});
|
||||
this.resultsCount += this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0);
|
||||
this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0);
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
@@ -214,9 +134,15 @@ export default {
|
||||
},
|
||||
clearTabData () {
|
||||
this.results = [];
|
||||
this.resultsCount = false;
|
||||
this.affectedCount = false;
|
||||
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
|
||||
this.resultsCount = 0;
|
||||
this.affectedCount = 0;
|
||||
},
|
||||
onKey (e) {
|
||||
if (this.isSelected) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'F9')
|
||||
this.runQuery(this.query);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -19,7 +19,7 @@
|
||||
:class="{'active': resultsetIndex === index}"
|
||||
@click="selectResultset(index)"
|
||||
>
|
||||
<a>{{ result.fields ? result.fields[0].orgTable : '' }} ({{ result.rows.length }})</a>
|
||||
<a>{{ result.fields ? result.fields[0].table : '' }} ({{ result.rows.length }})</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div ref="table" class="table table-hover">
|
||||
@@ -29,7 +29,7 @@
|
||||
v-for="(field, index) in fields"
|
||||
:key="index"
|
||||
class="th c-hand"
|
||||
:title="field.comment ? field.comment : false"
|
||||
:title="`${field.type} ${fieldLength(field) ? `(${fieldLength(field)})` : ''}`"
|
||||
>
|
||||
<div ref="columnResize" class="column-resizable">
|
||||
<div class="table-column-title" @click="sort(field.name)">
|
||||
@@ -73,13 +73,14 @@
|
||||
@contextmenu="contextMenu"
|
||||
/>
|
||||
</template>
|
||||
</basevirtualscroll>
|
||||
</BaseVirtualScroll>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
import { LONG_TEXT, BLOB } from 'common/fieldTypes';
|
||||
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
|
||||
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
|
||||
import TableContext from '@/components/WorkspaceQueryTableContext';
|
||||
@@ -114,7 +115,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getWorkspaceTab: 'workspaces/getWorkspaceTab',
|
||||
getWorkspace: 'workspaces/getWorkspace'
|
||||
}),
|
||||
workspaceSchema () {
|
||||
@@ -123,8 +123,11 @@ export default {
|
||||
primaryField () {
|
||||
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
|
||||
},
|
||||
isHardSort () {
|
||||
return this.mode === 'table' && this.localResults.length === 1000;
|
||||
},
|
||||
sortedResults () {
|
||||
if (this.currentSort) {
|
||||
if (this.currentSort && !this.isHardSort) {
|
||||
return [...this.localResults].sort((a, b) => {
|
||||
let modifier = 1;
|
||||
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
|
||||
@@ -141,14 +144,11 @@ export default {
|
||||
resultsWithRows () {
|
||||
return this.results.filter(result => result.rows);
|
||||
},
|
||||
tabProperties () {
|
||||
return this.getWorkspaceTab(this.tabUid);
|
||||
},
|
||||
fields () {
|
||||
return this.tabProperties && this.tabProperties.fields[this.resultsetIndex] ? this.tabProperties.fields[this.resultsetIndex] : [];
|
||||
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].fields : [];
|
||||
},
|
||||
keyUsage () {
|
||||
return this.tabProperties && this.tabProperties.keyUsage[this.resultsetIndex] ? this.tabProperties.keyUsage[this.resultsetIndex] : [];
|
||||
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : [];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -193,6 +193,10 @@ export default {
|
||||
|
||||
return length;
|
||||
},
|
||||
fieldLength (field) {
|
||||
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
|
||||
return field.numLength || field.datePrecision || field.charLength || 0;
|
||||
},
|
||||
keyName (key) {
|
||||
switch (key) {
|
||||
case 'pri':
|
||||
@@ -207,12 +211,12 @@ export default {
|
||||
},
|
||||
getTable (index) {
|
||||
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length)
|
||||
return this.resultsWithRows[index].fields[0].orgTable;
|
||||
return this.resultsWithRows[index].fields[0].table;
|
||||
return '';
|
||||
},
|
||||
getSchema (index) {
|
||||
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length)
|
||||
return this.resultsWithRows[index].fields[0].db;
|
||||
return this.resultsWithRows[index].fields[0].schema;
|
||||
return this.workspaceSchema;
|
||||
},
|
||||
getPrimaryValue (row) {
|
||||
@@ -220,15 +224,18 @@ export default {
|
||||
this.primaryField.alias,
|
||||
this.primaryField.name,
|
||||
`${this.primaryField.table}.${this.primaryField.alias}`,
|
||||
`${this.primaryField.table}.${this.primaryField.name}`
|
||||
`${this.primaryField.table}.${this.primaryField.name}`,
|
||||
`${this.primaryField.tableAlias}.${this.primaryField.alias}`,
|
||||
`${this.primaryField.tableAlias}.${this.primaryField.name}`
|
||||
].includes(prop));
|
||||
return row[primaryFieldName];
|
||||
},
|
||||
setLocalResults () {
|
||||
this.resetSort();
|
||||
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows ? this.resultsWithRows[this.resultsetIndex].rows.map(item => {
|
||||
return { ...item, _id: uidGen() };
|
||||
}) : [];
|
||||
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows
|
||||
? this.resultsWithRows[this.resultsetIndex].rows.map(item => {
|
||||
return { ...item, _id: uidGen() };
|
||||
})
|
||||
: [];
|
||||
},
|
||||
resizeResults () {
|
||||
if (this.$refs.resultTable) {
|
||||
@@ -263,7 +270,11 @@ export default {
|
||||
if (!this.primaryField)
|
||||
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') });
|
||||
else {
|
||||
const rowIDs = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => row[this.primaryField.name]);
|
||||
const rowIDs = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row =>
|
||||
row[this.primaryField.name] ||
|
||||
row[`${this.primaryField.table}.${this.primaryField.name}`] ||
|
||||
row[`${this.primaryField.tableAlias}.${this.primaryField.name}`]
|
||||
);
|
||||
const params = {
|
||||
primary: this.primaryField.name,
|
||||
schema: this.getSchema(this.resultsetIndex),
|
||||
@@ -333,6 +344,9 @@ export default {
|
||||
this.currentSortDir = 'asc';
|
||||
this.currentSort = field;
|
||||
}
|
||||
|
||||
if (this.isHardSort)
|
||||
this.$emit('hard-sort', { field: this.currentSort, dir: this.currentSortDir });
|
||||
},
|
||||
resetSort () {
|
||||
this.currentSort = '';
|
||||
|
@@ -27,7 +27,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
@@ -49,10 +48,6 @@ export default {
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
deleteConnection: 'connections/deleteConnection',
|
||||
showEditModal: 'application/showEditConnModal'
|
||||
}),
|
||||
showConfirmModal () {
|
||||
this.isConfirmModal = true;
|
||||
},
|
||||
|
@@ -20,6 +20,7 @@
|
||||
class="editable-field"
|
||||
:value.sync="editingContent"
|
||||
:key-usage="getKeyUsage(cKey)"
|
||||
size="small"
|
||||
@blur="editOFF"
|
||||
/>
|
||||
<template v-else>
|
||||
@@ -66,7 +67,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="editor-field-info">
|
||||
<div><b>{{ $t('word.size') }}</b>: {{ editingContent.length }}</div>
|
||||
<div><b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}</div>
|
||||
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,6 +158,8 @@ export default {
|
||||
typeFormat (val, type, precision) {
|
||||
if (!val) return val;
|
||||
|
||||
type = type.toUpperCase();
|
||||
|
||||
if (DATE.includes(type))
|
||||
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
|
||||
|
||||
@@ -177,6 +180,7 @@ export default {
|
||||
}
|
||||
|
||||
if (BIT.includes(type)) {
|
||||
if (typeof val === 'number') val = [val];
|
||||
const hex = Buffer.from(val).toString('hex');
|
||||
return hexToBinary(hex);
|
||||
}
|
||||
@@ -253,7 +257,10 @@ export default {
|
||||
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
|
||||
},
|
||||
foreignKeys () {
|
||||
return this.keyUsage.map(key => key.column);
|
||||
return this.keyUsage.map(key => key.field);
|
||||
},
|
||||
isEditable () {
|
||||
return this.fields ? !!(this.fields[0].schema && this.fields[0].table) : false;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -270,7 +277,7 @@ export default {
|
||||
if (field)
|
||||
type = field.type;
|
||||
|
||||
return type;
|
||||
return type.toLowerCase();
|
||||
},
|
||||
getFieldPrecision (cKey) {
|
||||
let length = 0;
|
||||
@@ -281,13 +288,24 @@ export default {
|
||||
return length;
|
||||
},
|
||||
getFieldObj (cKey) {
|
||||
return this.fields.filter(field =>
|
||||
field.name === cKey ||
|
||||
field.alias === cKey ||
|
||||
`${field.table}.${field.name}` === cKey ||
|
||||
`${field.table}.${field.alias}` === cKey ||
|
||||
`${field.table.toLowerCase()}.${field.name}` === cKey ||
|
||||
`${field.table.toLowerCase()}.${field.alias}` === cKey)[0];
|
||||
return this.fields.filter(field => {
|
||||
let fieldNames = [
|
||||
field.name,
|
||||
field.alias,
|
||||
`${field.table}.${field.name}`,
|
||||
`${field.table}.${field.alias}`,
|
||||
`${field.tableAlias}.${field.name}`,
|
||||
`${field.tableAlias}.${field.alias}`
|
||||
];
|
||||
|
||||
if (field.table)
|
||||
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
|
||||
|
||||
if (field.tableAlias)
|
||||
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
|
||||
|
||||
return fieldNames.includes(cKey);
|
||||
})[0];
|
||||
},
|
||||
isNull (value) {
|
||||
return value === null ? ' is-null' : '';
|
||||
@@ -296,7 +314,9 @@ export default {
|
||||
return bufferToBase64(val);
|
||||
},
|
||||
editON (event, content, field) {
|
||||
const type = this.getFieldType(field);
|
||||
if (!this.isEditable) return;
|
||||
|
||||
const type = this.getFieldType(field).toUpperCase(); ;
|
||||
this.originalContent = content;
|
||||
this.editingType = type;
|
||||
this.editingField = field;
|
||||
@@ -336,9 +356,7 @@ export default {
|
||||
this.$nextTick(() => document.querySelector('.editable-field').focus());
|
||||
});
|
||||
|
||||
const obj = {
|
||||
[field]: true
|
||||
};
|
||||
const obj = { [field]: true };
|
||||
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
|
||||
},
|
||||
editOFF () {
|
||||
@@ -405,7 +423,7 @@ export default {
|
||||
this.$emit('select-row', event, row);
|
||||
},
|
||||
getKeyUsage (keyName) {
|
||||
return this.keyUsage.find(key => key.column === keyName);
|
||||
return this.keyUsage.find(key => key.field === keyName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -1,19 +1,39 @@
|
||||
<template>
|
||||
<div 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-footer">
|
||||
<div class="workspace-query-buttons">
|
||||
<div class="dropdown">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-dark btn-sm mr-0 pr-1"
|
||||
:class="{'loading':isQuering}"
|
||||
title="F5"
|
||||
@click="reloadTable"
|
||||
>
|
||||
<span>{{ $t('word.refresh') }}</span>
|
||||
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh ml-1" />
|
||||
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h ml-1" />
|
||||
</button>
|
||||
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
|
||||
<i class="mdi mdi-24px mdi-menu-down" />
|
||||
</div>
|
||||
<div class="menu px-3">
|
||||
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
|
||||
<input
|
||||
v-model="autorefreshTimer"
|
||||
class="slider no-border"
|
||||
type="range"
|
||||
min="0"
|
||||
max="30"
|
||||
step="1"
|
||||
@change="setRefreshInterval"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-link btn-sm"
|
||||
:class="{'loading':isQuering}"
|
||||
@click="reloadTable"
|
||||
>
|
||||
<span>{{ $t('word.refresh') }}</span>
|
||||
<i class="mdi mdi-24px mdi-refresh ml-1" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-link btn-sm"
|
||||
:class="{'disabled':isQuering}"
|
||||
class="btn btn-dark btn-sm"
|
||||
@click="showAddModal"
|
||||
>
|
||||
<span>{{ $t('word.add') }}</span>
|
||||
@@ -22,7 +42,10 @@
|
||||
</div>
|
||||
<div class="workspace-query-info">
|
||||
<div v-if="results.length && results[0].rows">
|
||||
{{ $t('word.results') }}: <b>{{ results[0].rows.length }}</b>
|
||||
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
|
||||
</div>
|
||||
<div v-if="results.length && results[0].rows && tableInfo && results[0].rows.length < tableInfo.rows">
|
||||
{{ $t('word.total') }}: <b>{{ tableInfo.rows.toLocaleString() }}</b> <small>({{ $t('word.approximately') }})</small>
|
||||
</div>
|
||||
<div v-if="workspace.breadcrumbs.database">
|
||||
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b>
|
||||
@@ -33,7 +56,6 @@
|
||||
<div class="workspace-query-results column col-12">
|
||||
<WorkspaceQueryTable
|
||||
v-if="results"
|
||||
v-show="!isQuering"
|
||||
ref="queryTable"
|
||||
:results="results"
|
||||
:tab-uid="tabUid"
|
||||
@@ -41,10 +63,13 @@
|
||||
mode="table"
|
||||
@update-field="updateField"
|
||||
@delete-selected="deleteSelected"
|
||||
@hard-sort="hardSort"
|
||||
/>
|
||||
</div>
|
||||
<ModalNewTableRow
|
||||
v-if="isAddModal"
|
||||
:fields="fields"
|
||||
:key-usage="keyUsage"
|
||||
:tab-uid="tabUid"
|
||||
@hide="hideAddModal"
|
||||
@reload="reloadTable"
|
||||
@@ -75,10 +100,11 @@ export default {
|
||||
tabUid: 'data',
|
||||
isQuering: false,
|
||||
results: [],
|
||||
fields: [],
|
||||
keyUsage: [],
|
||||
lastTable: null,
|
||||
isAddModal: false
|
||||
isAddModal: false,
|
||||
autorefreshTimer: 0,
|
||||
refreshInterval: null,
|
||||
sortParams: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -90,13 +116,29 @@ export default {
|
||||
},
|
||||
isSelected () {
|
||||
return this.workspace.selected_tab === 'data';
|
||||
},
|
||||
fields () {
|
||||
return this.results.length ? this.results[0].fields : [];
|
||||
},
|
||||
keyUsage () {
|
||||
return this.results.length ? this.results[0].keys : [];
|
||||
},
|
||||
tableInfo () {
|
||||
try {
|
||||
return this.workspace.structure.find(db => db.name === this.schema).tables.find(table => table.name === this.table);
|
||||
}
|
||||
catch (err) {
|
||||
return { rows: 0 };
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
table () {
|
||||
if (this.isSelected) {
|
||||
this.sortParams = {};
|
||||
this.getTableData();
|
||||
this.lastTable = this.table;
|
||||
this.$refs.queryTable.resetSort();
|
||||
}
|
||||
},
|
||||
isSelected (val) {
|
||||
@@ -108,40 +150,31 @@ export default {
|
||||
},
|
||||
created () {
|
||||
this.getTableData();
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
clearInterval(this.refreshInterval);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification',
|
||||
setTabFields: 'workspaces/setTabFields',
|
||||
setTabKeyUsage: 'workspaces/setTabKeyUsage'
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
async getTableData () {
|
||||
async getTableData (sortParams) {
|
||||
if (!this.table) return;
|
||||
this.isQuering = true;
|
||||
this.results = [];
|
||||
const fieldsArr = [];
|
||||
const keysArr = [];
|
||||
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
|
||||
|
||||
// if table changes clear cached values
|
||||
if (this.lastTable !== this.table)
|
||||
this.results = [];
|
||||
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
schema: this.schema,
|
||||
table: this.workspace.breadcrumbs.table
|
||||
table: this.workspace.breadcrumbs.table,
|
||||
sortParams
|
||||
};
|
||||
|
||||
try { // Columns data
|
||||
const { status, response } = await Tables.getTableColumns(params);
|
||||
if (status === 'success') {
|
||||
this.fields = response;// Needed to add new rows
|
||||
fieldsArr.push(response);
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
try { // Table data
|
||||
const { status, response } = await Tables.getTableData(params);
|
||||
|
||||
@@ -154,71 +187,42 @@ export default {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
try { // Key usage (foreign keys)
|
||||
const { status, response } = await Tables.getKeyUsage(params);
|
||||
|
||||
if (status === 'success') {
|
||||
this.keyUsage = response;// Needed to add new rows
|
||||
keysArr.push(response);
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: fieldsArr });
|
||||
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: keysArr });
|
||||
this.isQuering = false;
|
||||
},
|
||||
getTable () {
|
||||
return this.table;
|
||||
},
|
||||
reloadTable () {
|
||||
this.getTableData();
|
||||
this.getTableData(this.sortParams);
|
||||
},
|
||||
hardSort (sortParams) {
|
||||
this.sortParams = sortParams;
|
||||
this.getTableData(sortParams);
|
||||
},
|
||||
showAddModal () {
|
||||
this.isAddModal = true;
|
||||
},
|
||||
hideAddModal () {
|
||||
this.isAddModal = false;
|
||||
},
|
||||
onKey (e) {
|
||||
if (this.isSelected) {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'F5')
|
||||
this.reloadTable();
|
||||
}
|
||||
},
|
||||
setRefreshInterval () {
|
||||
if (this.refreshInterval)
|
||||
clearInterval(this.refreshInterval);
|
||||
|
||||
if (+this.autorefreshTimer) {
|
||||
this.refreshInterval = setInterval(() => {
|
||||
if (!this.isQuering)
|
||||
this.reloadTable();
|
||||
}, this.autorefreshTimer * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.workspace-tabs {
|
||||
align-content: baseline;
|
||||
|
||||
.workspace-query-runner {
|
||||
.workspace-query-runner-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.3rem 0.6rem 0.4rem;
|
||||
align-items: center;
|
||||
|
||||
.workspace-query-buttons {
|
||||
display: flex;
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
color: $body-font-color;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-info {
|
||||
display: flex;
|
||||
|
||||
> div + div {
|
||||
padding-left: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -41,7 +41,28 @@ module.exports = {
|
||||
insert: 'Insert',
|
||||
connecting: 'Connecting',
|
||||
name: 'Name',
|
||||
collation: 'Collation'
|
||||
collation: 'Collation',
|
||||
clear: 'Clear',
|
||||
options: 'Options',
|
||||
autoRefresh: 'Auto-refresh',
|
||||
indexes: 'Indexes',
|
||||
foreignKeys: 'Foreign keys',
|
||||
length: 'Length',
|
||||
unsigned: 'Unsigned',
|
||||
default: 'Default',
|
||||
comment: 'Comment',
|
||||
key: 'Key | Keys',
|
||||
order: 'Order',
|
||||
expression: 'Expression',
|
||||
autoIncrement: 'Auto Increment',
|
||||
engine: 'Engine',
|
||||
field: 'Field | Fields',
|
||||
approximately: 'Approximately',
|
||||
total: 'Total',
|
||||
table: 'Table',
|
||||
discard: 'Discard',
|
||||
stay: 'Stay',
|
||||
author: 'Author'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Welcome to Antares SQL Client!',
|
||||
@@ -78,7 +99,32 @@ module.exports = {
|
||||
databaseName: 'Database name',
|
||||
serverDefault: 'Server default',
|
||||
deleteDatabase: 'Delete database',
|
||||
editDatabase: 'Edit database'
|
||||
editDatabase: 'Edit database',
|
||||
clearChanges: 'Clear changes',
|
||||
addNewField: 'Add new field',
|
||||
manageIndexes: 'Manage indexes',
|
||||
manageForeignKeys: 'Manage foreign keys',
|
||||
allowNull: 'Allow NULL',
|
||||
zeroFill: 'Zero fill',
|
||||
customValue: 'Custom value',
|
||||
onUpdate: 'On update',
|
||||
deleteField: 'Delete field',
|
||||
createNewIndex: 'Create new index',
|
||||
addToIndex: 'Add to index',
|
||||
createNewTable: 'Create new table',
|
||||
emptyTable: 'Empty table',
|
||||
deleteTable: 'Delete table',
|
||||
emptyCorfirm: 'Do you confirm to empty',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
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'
|
||||
},
|
||||
// Date and Time
|
||||
short: {
|
||||
|
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 34 32"><path fill="#444" d="M21.576 3.59c-1.115-.994-2.465-.595-3.798.588a9.407 9.407 0 00-.591.579c-2.279 2.418-4.395 6.897-5.053 10.318.256.519.456 1.182.588 1.688.034.13.064.252.089.355.058.245.089.405.089.405s-.02-.077-.104-.321l-.055-.158a1.44 1.44 0 00-.035-.087c-.149-.346-.56-1.075-.741-1.393-.155.457-.292.884-.406 1.271.523.956.841 2.595.841 2.595s-.028-.106-.159-.477c-.117-.328-.697-1.345-.835-1.583-.235.869-.329 1.455-.244 1.598.164.277.32.754.457 1.282.309 1.189.524 2.637.524 2.637l.019.244c-.043.999-.017 2.034.06 2.97.103 1.239.295 2.303.541 2.873l.167-.091c-.361-1.122-.508-2.593-.444-4.289.097-2.593.694-5.719 1.796-8.978 1.863-4.919 4.447-8.866 6.811-10.751-2.155 1.947-5.073 8.248-5.946 10.581-.978 2.613-1.671 5.065-2.088 7.414.721-2.202 3.05-3.149 3.05-3.149s1.143-1.409 2.478-3.422c-.8.182-2.113.495-2.553.68-.649.272-.824.365-.824.365s2.102-1.28 3.905-1.86c2.48-3.906 5.182-9.456 2.461-11.884z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 34 32"><path fill="#fff" d="M21.576 3.59c-1.115-.994-2.465-.595-3.798.588a9.407 9.407 0 00-.591.579c-2.279 2.418-4.395 6.897-5.053 10.318.256.519.456 1.182.588 1.688.034.13.064.252.089.355.058.245.089.405.089.405s-.02-.077-.104-.321l-.055-.158a1.44 1.44 0 00-.035-.087c-.149-.346-.56-1.075-.741-1.393-.155.457-.292.884-.406 1.271.523.956.841 2.595.841 2.595s-.028-.106-.159-.477c-.117-.328-.697-1.345-.835-1.583-.235.869-.329 1.455-.244 1.598.164.277.32.754.457 1.282.309 1.189.524 2.637.524 2.637l.019.244c-.043.999-.017 2.034.06 2.97.103 1.239.295 2.303.541 2.873l.167-.091c-.361-1.122-.508-2.593-.444-4.289.097-2.593.694-5.719 1.796-8.978 1.863-4.919 4.447-8.866 6.811-10.751-2.155 1.947-5.073 8.248-5.946 10.581-.978 2.613-1.671 5.065-2.088 7.414.721-2.202 3.05-3.149 3.05-3.149s1.143-1.409 2.478-3.422c-.8.182-2.113.495-2.553.68-.649.272-.824.365-.824.365s2.102-1.28 3.905-1.86c2.48-3.906 5.182-9.456 2.461-11.884z"/></svg>
|
Before Width: | Height: | Size: 1004 B After Width: | Height: | Size: 1004 B |
@@ -30,6 +30,10 @@ export default class {
|
||||
return ipcRenderer.invoke('get-variables', uid);
|
||||
}
|
||||
|
||||
static getEngines (uid) {
|
||||
return ipcRenderer.invoke('get-engines', uid);
|
||||
}
|
||||
|
||||
static useSchema (params) {
|
||||
return ipcRenderer.invoke('use-schema', params);
|
||||
}
|
||||
|
@@ -10,6 +10,10 @@ export default class {
|
||||
return ipcRenderer.invoke('get-table-data', params);
|
||||
}
|
||||
|
||||
static getTableIndexes (params) {
|
||||
return ipcRenderer.invoke('get-table-indexes', params);
|
||||
}
|
||||
|
||||
static getKeyUsage (params) {
|
||||
return ipcRenderer.invoke('get-key-usage', params);
|
||||
}
|
||||
@@ -29,4 +33,20 @@ export default class {
|
||||
static getForeignList (params) {
|
||||
return ipcRenderer.invoke('get-foreign-list', params);
|
||||
}
|
||||
|
||||
static createTable (params) {
|
||||
return ipcRenderer.invoke('create-table', params);
|
||||
}
|
||||
|
||||
static alterTable (params) {
|
||||
return ipcRenderer.invoke('alter-table', params);
|
||||
}
|
||||
|
||||
static truncateTable (params) {
|
||||
return ipcRenderer.invoke('truncate-table', params);
|
||||
}
|
||||
|
||||
static dropTable (params) {
|
||||
return ipcRenderer.invoke('drop-table', params);
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,12 @@
|
||||
|
||||
@mixin type-colors($types) {
|
||||
$numbers: ('int','tinyint','smallint','mediumint','float','double','decimal');
|
||||
|
||||
@each $type, $color in $types {
|
||||
.type-#{$type} {
|
||||
color: $color;
|
||||
|
||||
@if $type == "number" {
|
||||
@if index($numbers, $type) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@@ -12,35 +15,41 @@
|
||||
|
||||
@include type-colors(
|
||||
(
|
||||
"char": seagreen,
|
||||
"varchar": seagreen,
|
||||
"text": seagreen,
|
||||
"mediumtext": seagreen,
|
||||
"longtext": seagreen,
|
||||
"int": cornflowerblue,
|
||||
"tinyint": cornflowerblue,
|
||||
"smallint": cornflowerblue,
|
||||
"mediumint": cornflowerblue,
|
||||
"float": cornflowerblue,
|
||||
"double": cornflowerblue,
|
||||
"decimal": cornflowerblue,
|
||||
"bigint": cornflowerblue,
|
||||
"datetime": coral,
|
||||
"date": coral,
|
||||
"time": coral,
|
||||
"timestamp": coral,
|
||||
"bit": lightskyblue,
|
||||
"blob": darkorchid,
|
||||
"mediumblob": darkorchid,
|
||||
"longblob": darkorchid,
|
||||
"enum": gold,
|
||||
"set": gold,
|
||||
"unknown": gray,
|
||||
"char": $string-color,
|
||||
"varchar": $string-color,
|
||||
"text": $string-color,
|
||||
"tinytext": $string-color,
|
||||
"mediumtext": $string-color,
|
||||
"longtext": $string-color,
|
||||
"json": $string-color,
|
||||
"int": $number-color,
|
||||
"tinyint": $number-color,
|
||||
"smallint": $number-color,
|
||||
"mediumint": $number-color,
|
||||
"float": $number-color,
|
||||
"double": $number-color,
|
||||
"decimal": $number-color,
|
||||
"bigint": $number-color,
|
||||
"datetime": $date-color,
|
||||
"date": $date-color,
|
||||
"time": $date-color,
|
||||
"year": $date-color,
|
||||
"timestamp": $date-color,
|
||||
"bit": $bit-color,
|
||||
"binary": $blob-color,
|
||||
"varbinary": $blob-color,
|
||||
"blob": $blob-color,
|
||||
"tinyblob": $blob-color,
|
||||
"mediumblob": $blob-color,
|
||||
"longblob": $blob-color,
|
||||
"enum": $enum-color,
|
||||
"set": $enum-color,
|
||||
"unknown": $unknown-color,
|
||||
)
|
||||
);
|
||||
|
||||
.is-null {
|
||||
color: gray;
|
||||
color: $unknown-color;
|
||||
|
||||
&::after {
|
||||
content: "NULL";
|
||||
|
@@ -1,18 +1,25 @@
|
||||
.column-key {
|
||||
transform: rotate(90deg);
|
||||
transform: rotate(45deg);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.5;
|
||||
margin-right: 0.2rem;
|
||||
|
||||
&.key-pri {
|
||||
&.key-pri,
|
||||
&.key-PRIMARY {
|
||||
color: goldenrod;
|
||||
}
|
||||
|
||||
&.key-uni {
|
||||
&.key-uni,
|
||||
&.key-UNIQUE {
|
||||
color: deepskyblue;
|
||||
}
|
||||
|
||||
&.key-mul {
|
||||
&.key-mul,
|
||||
&.key-INDEX {
|
||||
color: palegreen;
|
||||
}
|
||||
|
||||
&.key-FULLTEXT {
|
||||
color: mediumvioletred;
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,14 @@ $success-color: #32b643;
|
||||
$error-color: #de3b28;
|
||||
$warning-color: #e0a40c;
|
||||
|
||||
$string-color: seagreen;
|
||||
$number-color: cornflowerblue;
|
||||
$date-color: coral;
|
||||
$bit-color: lightskyblue;
|
||||
$blob-color: darkorchid;
|
||||
$enum-color: gold;
|
||||
$unknown-color: gray;
|
||||
|
||||
/* Sizes */
|
||||
$titlebar-height: 1.5rem;
|
||||
$settingbar-width: 3rem;
|
||||
|
@@ -19,12 +19,24 @@ body {
|
||||
@include padding-variant(3, $unit-3);
|
||||
@include padding-variant(4, $unit-4);
|
||||
|
||||
.btn.btn-gray {
|
||||
color: #fff;
|
||||
background: $bg-color-gray;
|
||||
.btn {
|
||||
&.btn-gray {
|
||||
color: #fff;
|
||||
background: $bg-color-gray;
|
||||
|
||||
&:hover {
|
||||
background: $bg-color;
|
||||
&:hover {
|
||||
background: $bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-dark {
|
||||
color: #fff;
|
||||
background: $bg-color-light;
|
||||
border-color: $bg-color-light;
|
||||
|
||||
&:hover {
|
||||
background: $bg-color-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +49,12 @@ body {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.no-border {
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.bg-checkered {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
|
||||
@@ -46,6 +64,38 @@ body {
|
||||
background-size: 2em 2em;
|
||||
}
|
||||
|
||||
.workspace-tabs {
|
||||
align-content: baseline;
|
||||
|
||||
.workspace-query-runner {
|
||||
.workspace-query-runner-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.3rem 0.6rem 0.4rem;
|
||||
align-items: center;
|
||||
|
||||
.workspace-query-buttons {
|
||||
display: flex;
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
color: $body-font-color;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-info {
|
||||
display: flex;
|
||||
|
||||
> div + div {
|
||||
padding-left: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollbars
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
@@ -130,6 +180,12 @@ body {
|
||||
|
||||
.form-select {
|
||||
cursor: pointer;
|
||||
|
||||
&.small-select {
|
||||
height: 1rem;
|
||||
font-size: 0.7rem;
|
||||
padding: 1px 0.4rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-select,
|
||||
@@ -141,6 +197,11 @@ body {
|
||||
background-color: $bg-color-gray;
|
||||
}
|
||||
|
||||
.form-input.is-error,
|
||||
.form-select.is-error {
|
||||
background-color: $bg-color-gray;
|
||||
}
|
||||
|
||||
.form-input:not(:placeholder-shown):invalid:focus {
|
||||
background: $bg-color-gray;
|
||||
}
|
||||
@@ -173,3 +234,7 @@ body {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: $body-font-color;
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ export default {
|
||||
app_version: process.env.PACKAGE_VERSION || 0,
|
||||
is_loading: false,
|
||||
is_new_modal: false,
|
||||
is_edit_modal: false,
|
||||
is_setting_modal: false,
|
||||
selected_setting_tab: 'general',
|
||||
selected_conection: {},
|
||||
@@ -20,7 +19,6 @@ export default {
|
||||
appVersion: state => state.app_version,
|
||||
getSelectedConnection: state => state.selected_conection,
|
||||
isNewModal: state => state.is_new_modal,
|
||||
isEditModal: state => state.is_edit_modal,
|
||||
isSettingModal: state => state.is_setting_modal,
|
||||
selectedSettingTab: state => state.selected_setting_tab,
|
||||
getUpdateStatus: state => state.update_status,
|
||||
@@ -36,13 +34,6 @@ export default {
|
||||
HIDE_NEW_CONNECTION_MODAL (state) {
|
||||
state.is_new_modal = false;
|
||||
},
|
||||
SHOW_EDIT_CONNECTION_MODAL (state, connection) {
|
||||
state.is_edit_modal = true;
|
||||
state.selected_conection = connection;
|
||||
},
|
||||
HIDE_EDIT_CONNECTION_MODAL (state) {
|
||||
state.is_edit_modal = false;
|
||||
},
|
||||
SHOW_SETTING_MODAL (state, tab) {
|
||||
state.selected_setting_tab = tab;
|
||||
state.is_setting_modal = true;
|
||||
@@ -68,12 +59,6 @@ export default {
|
||||
hideNewConnModal ({ commit }) {
|
||||
commit('HIDE_NEW_CONNECTION_MODAL');
|
||||
},
|
||||
showEditConnModal ({ commit }, connection) {
|
||||
commit('SHOW_EDIT_CONNECTION_MODAL', connection);
|
||||
},
|
||||
hideEditConnModal ({ commit }) {
|
||||
commit('HIDE_EDIT_CONNECTION_MODAL');
|
||||
},
|
||||
showSettingModal ({ commit }, tab) {
|
||||
commit('SHOW_SETTING_MODAL', tab);
|
||||
},
|
||||
|
@@ -13,7 +13,7 @@ export default {
|
||||
return connection.name
|
||||
? connection.name
|
||||
: connection.ask
|
||||
? ''
|
||||
? `${connection.host}:${connection.port}`
|
||||
: `${connection.user + '@'}${connection.host}:${connection.port}`;
|
||||
}
|
||||
},
|
||||
|
@@ -3,14 +3,17 @@ import Connection from '@/ipc-api/Connection';
|
||||
import Database from '@/ipc-api/Database';
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
const tabIndex = [];
|
||||
let lastSchema = '';
|
||||
let lastBreadcrumbs = {};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
strict: true,
|
||||
state: {
|
||||
workspaces: [],
|
||||
selected_workspace: null
|
||||
selected_workspace: null,
|
||||
has_unsaved_changes: false,
|
||||
is_unsaved_discard_modal: false,
|
||||
pending_breadcrumbs: {}
|
||||
},
|
||||
getters: {
|
||||
getSelected: state => {
|
||||
@@ -35,38 +38,83 @@ export default {
|
||||
return state.workspaces
|
||||
.filter(workspace => workspace.connected)
|
||||
.map(workspace => workspace.uid);
|
||||
},
|
||||
isUnsavedDiscardModal: state => {
|
||||
return state.is_unsaved_discard_modal;
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
SELECT_WORKSPACE (state, uid) {
|
||||
state.selected_workspace = uid;
|
||||
},
|
||||
ADD_CONNECTED (state, { uid, structure }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, structure, connected: true } : workspace);
|
||||
ADD_CONNECTED (state, { uid, client, dataTypes, indexTypes, structure }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||
? {
|
||||
...workspace,
|
||||
client,
|
||||
dataTypes,
|
||||
indexTypes,
|
||||
structure,
|
||||
connected: true
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
REMOVE_CONNECTED (state, uid) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, structure: {}, connected: false } : workspace);
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||
? {
|
||||
...workspace,
|
||||
structure: {},
|
||||
connected: false
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
REFRESH_STRUCTURE (state, { uid, structure }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, structure } : workspace);
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||
? {
|
||||
...workspace,
|
||||
structure
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
REFRESH_COLLATIONS (state, { uid, collations }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, collations } : workspace);
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||
? {
|
||||
...workspace,
|
||||
collations
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
REFRESH_VARIABLES (state, { uid, variables }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, variables } : workspace);
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||
? {
|
||||
...workspace,
|
||||
variables
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
REFRESH_ENGINES (state, { uid, engines }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||
? {
|
||||
...workspace,
|
||||
engines
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
ADD_WORKSPACE (state, workspace) {
|
||||
state.workspaces.push(workspace);
|
||||
},
|
||||
CHANGE_BREADCRUMBS (state, { uid, breadcrumbs }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, breadcrumbs } : workspace);
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||
? {
|
||||
...workspace,
|
||||
breadcrumbs
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
NEW_TAB (state, uid) {
|
||||
NEW_TAB (state, { uid, tab }) {
|
||||
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
|
||||
|
||||
const newTab = {
|
||||
uid: uidGen('T'),
|
||||
uid: tab,
|
||||
index: tabIndex[uid],
|
||||
selected: false,
|
||||
type: 'query',
|
||||
@@ -132,10 +180,19 @@ export default {
|
||||
else
|
||||
return workspace;
|
||||
});
|
||||
},
|
||||
SET_UNSAVED_CHANGES (state, val) {
|
||||
state.has_unsaved_changes = !!val;
|
||||
},
|
||||
SET_UNSAVED_DISCARD_MODAL (state, val) {
|
||||
state.is_unsaved_discard_modal = !!val;
|
||||
},
|
||||
SET_PENDING_BREADCRUMBS (state, payload) {
|
||||
state.pending_breadcrumbs = payload;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
selectWorkspace ({ commit, dispatch }, uid) {
|
||||
selectWorkspace ({ commit }, uid) {
|
||||
commit('SELECT_WORKSPACE', uid);
|
||||
},
|
||||
async connectWorkspace ({ dispatch, commit }, connection) {
|
||||
@@ -144,9 +201,26 @@ export default {
|
||||
if (status === 'error')
|
||||
dispatch('notifications/addNotification', { status, message: response }, { root: true });
|
||||
else {
|
||||
commit('ADD_CONNECTED', { uid: connection.uid, structure: response });
|
||||
let dataTypes = [];
|
||||
let indexTypes = [];
|
||||
|
||||
switch (connection.client) {
|
||||
case 'mysql':
|
||||
case 'maria':
|
||||
dataTypes = require('common/data-types/mysql');
|
||||
indexTypes = require('common/index-types/mysql');
|
||||
break;
|
||||
}
|
||||
commit('ADD_CONNECTED', {
|
||||
uid: connection.uid,
|
||||
client: connection.client,
|
||||
dataTypes,
|
||||
indexTypes,
|
||||
structure: response
|
||||
});
|
||||
dispatch('refreshCollations', connection.uid);
|
||||
dispatch('refreshVariables', connection.uid);
|
||||
dispatch('refreshEngines', connection.uid);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
@@ -189,6 +263,18 @@ export default {
|
||||
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
|
||||
}
|
||||
},
|
||||
async refreshEngines ({ dispatch, commit }, uid) {
|
||||
try {
|
||||
const { status, response } = await Database.getEngines(uid);
|
||||
if (status === 'error')
|
||||
dispatch('notifications/addNotification', { status, message: response }, { root: true });
|
||||
else
|
||||
commit('REFRESH_ENGINES', { uid, engines: response });
|
||||
}
|
||||
catch (err) {
|
||||
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
|
||||
}
|
||||
},
|
||||
removeConnected ({ commit }, uid) {
|
||||
Connection.disconnect(uid);
|
||||
commit('REMOVE_CONNECTED', uid);
|
||||
@@ -221,17 +307,40 @@ export default {
|
||||
|
||||
if (getters.getWorkspace(uid).tabs.length < 3)
|
||||
dispatch('newTab', uid);
|
||||
|
||||
dispatch('setUnsavedChanges', false);
|
||||
},
|
||||
changeBreadcrumbs ({ commit, getters }, payload) {
|
||||
if (lastSchema !== payload.schema) {
|
||||
Database.useSchema({ uid: getters.getSelected, schema: payload.schema });
|
||||
lastSchema = payload.schema;
|
||||
changeBreadcrumbs ({ state, commit, getters }, payload) {
|
||||
if (state.has_unsaved_changes) {
|
||||
commit('SET_UNSAVED_DISCARD_MODAL', true);
|
||||
commit('SET_PENDING_BREADCRUMBS', payload);
|
||||
return;
|
||||
}
|
||||
|
||||
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: payload });
|
||||
const breadcrumbsObj = {
|
||||
schema: null,
|
||||
table: null,
|
||||
trigger: null,
|
||||
procedure: null,
|
||||
scheduler: null
|
||||
};
|
||||
|
||||
const hasLastChildren = Object.keys(lastBreadcrumbs).filter(b => b !== 'schema').some(b => lastBreadcrumbs[b]);
|
||||
const hasChildren = Object.keys(payload).filter(b => b !== 'schema').some(b => payload[b]);
|
||||
|
||||
if (lastBreadcrumbs.schema === payload.schema && hasLastChildren && !hasChildren) return;
|
||||
|
||||
if (lastBreadcrumbs.schema !== payload.schema)
|
||||
Database.useSchema({ uid: getters.getSelected, schema: payload.schema });
|
||||
|
||||
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...breadcrumbsObj, ...payload } });
|
||||
lastBreadcrumbs = { ...breadcrumbsObj, ...payload };
|
||||
},
|
||||
newTab ({ commit }, uid) {
|
||||
commit('NEW_TAB', uid);
|
||||
const tab = uidGen('T');
|
||||
|
||||
commit('NEW_TAB', { uid, tab });
|
||||
commit('SELECT_TAB', { uid, tab });
|
||||
},
|
||||
removeTab ({ commit }, payload) {
|
||||
commit('REMOVE_TAB', payload);
|
||||
@@ -244,6 +353,18 @@ export default {
|
||||
},
|
||||
setTabKeyUsage ({ commit }, payload) {
|
||||
commit('SET_TAB_KEY_USAGE', payload);
|
||||
},
|
||||
setUnsavedChanges ({ commit }, val) {
|
||||
commit('SET_UNSAVED_CHANGES', val);
|
||||
},
|
||||
discardUnsavedChanges ({ state, commit, dispatch }) {
|
||||
dispatch('setUnsavedChanges', false);
|
||||
dispatch('changeBreadcrumbs', state.pending_breadcrumbs);
|
||||
commit('SET_UNSAVED_DISCARD_MODAL', false);
|
||||
commit('SET_PENDING_BREADCRUMBS', {});
|
||||
},
|
||||
closeUnsavedChangesModal ({ commit }) {
|
||||
commit('SET_UNSAVED_DISCARD_MODAL', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user