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

Compare commits

..

53 Commits

Author SHA1 Message Date
8cafade8b1 chore(release): 0.0.11 2020-12-15 17:24:04 +01:00
d13b708377 chore: update REDME.md 2020-12-15 17:23:24 +01:00
206597e5b8 feat: foreign keys management 2020-12-15 17:08:36 +01:00
c5458159d1 fix: unable to switch tabs when no table selected 2020-12-11 18:22:07 +01:00
1476e899d1 feat: auto focus on first input in modals 2020-12-11 18:09:17 +01:00
797ab70e7c chore: update links 2020-12-11 16:05:32 +01:00
f81312aeb0 feat: query tabs auto focus 2020-12-11 15:55:18 +01:00
3ed5ea023e fix: some properties do not reset after fields changes 2020-12-11 12:57:24 +01:00
9291a7a7b4 fix: file field editor not show 2020-12-10 15:15:32 +01:00
5cfdc9b92d fix: wrong field type detection 2020-12-09 18:22:46 +01:00
15b08d7ea8 fix: data tab sort not maintained at refresh 2020-12-08 18:41:08 +01:00
acebe435ff fix: improved changes dedection in props tab 2020-12-07 19:11:29 +01:00
5712b80022 feat: improved data table sorts 2020-12-07 17:51:48 +01:00
d38583262e fix: deletion of rows with non-numeric ID 2020-12-07 15:07:59 +01:00
e0e2131981 chore: update README.md 2020-12-04 11:43:27 +01:00
7470bddd70 chore(release): 0.0.10 2020-12-04 11:20:51 +01:00
33d1fa2290 feat: unsaved changes reminder 2020-12-04 11:19:16 +01:00
a4122b4eaa feat: drop and truncate tables 2020-12-03 16:15:10 +01:00
e6602d1bfa feat: create new tables 2020-12-03 13:00:54 +01:00
f8cf90a89e fix: index deletion issue 2020-12-01 17:29:16 +01:00
41505bde65 feat: index management 2020-12-01 16:48:20 +01:00
8ebc3bce92 chore: remove deprecated eslint-plugin-standard 2020-11-30 18:24:12 +01:00
45e9cdc591 Merge pull request #40 from Fabio286/dependabot/npm_and_yarn/eslint-plugin-standard-5.0.0
build(deps-dev): bump eslint-plugin-standard from 4.1.0 to 5.0.0
2020-11-30 18:20:47 +01:00
3cbfc0e148 chore: update README.md 2020-11-30 10:04:31 +01:00
a47e9e1b1f Merge pull request #41 from EStarium/dependabot/npm_and_yarn/electron-11.0.2
build(deps-dev): bump electron from 10.1.6 to 11.0.2
2020-11-28 09:16:34 +01:00
e95d29c7c3 feat: approximate totals in table tata tab 2020-11-25 11:47:35 +01:00
e954f04828 refactor: improved structure for table options modal 2020-11-23 12:25:44 +01:00
dependabot[bot]
85c800f85b build(deps-dev): bump electron from 10.1.6 to 11.0.2
Bumps [electron](https://github.com/electron/electron) from 10.1.6 to 11.0.2.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v10.1.6...v11.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-23 05:58:15 +00:00
dependabot[bot]
e0482244d7 build(deps-dev): bump eslint-plugin-standard from 4.1.0 to 5.0.0
Bumps [eslint-plugin-standard](https://github.com/standard/eslint-plugin-standard) from 4.1.0 to 5.0.0.
- [Release notes](https://github.com/standard/eslint-plugin-standard/releases)
- [Commits](https://github.com/standard/eslint-plugin-standard/compare/v4.1.0...v5.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-23 05:47:45 +00:00
27769f204f feat: display all keys in properties tab 2020-11-20 17:24:02 +01:00
dfb24c65f3 fix: sqlEscaper function wrong quotes conversion 2020-11-20 09:16:18 +01:00
0fe71572a5 fix: some problems with properties and data tabs when changing database from sidebar 2020-11-18 18:21:15 +01:00
db577bfef0 ci: temporary removed Linux ARM build 2020-11-16 17:17:33 +01:00
0805b96a75 feat: tables options edit 2020-11-16 17:16:39 +01:00
e49823f5c4 chore(release): 0.0.9 2020-11-13 17:55:43 +01:00
76351005b4 chore: update dependencies 2020-11-13 17:49:09 +01:00
3e5770f7de fix: zero fill field option was not saved 2020-11-13 16:37:52 +01:00
242ddec744 feat: table fields deletion 2020-11-13 16:19:59 +01:00
07654039b6 feat: table fields addition 2020-11-13 15:04:51 +01:00
249926b8e0 feat: ability to edit table fields 2020-11-13 12:39:40 +01:00
ae47a978c1 Merge pull request #39 from EStarium/dependabot/npm_and_yarn/node-sass-5.0.0
build(deps-dev): bump node-sass from 4.14.1 to 5.0.0
2020-11-02 09:06:12 +01:00
dependabot[bot]
bc53b0b332 build(deps-dev): bump node-sass from 4.14.1 to 5.0.0
Bumps [node-sass](https://github.com/sass/node-sass) from 4.14.1 to 5.0.0.
- [Release notes](https://github.com/sass/node-sass/releases)
- [Changelog](https://github.com/sass/node-sass/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sass/node-sass/compare/v4.14.1...v5.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-02 05:52:03 +00:00
d4175bcbda ci: ARM build configuration 2020-10-28 12:58:16 +01:00
c9ba2e5962 fix: F9 key shortcut refresh all query tabs instead of just selected one 2020-10-27 17:04:39 +01:00
2e49d86677 refactor(core): improved how application gets query fields and keys 2020-10-27 16:41:00 +01:00
c393f86947 fix: issue with tabs horizontal scroll with wheel 2020-10-26 09:28:29 +01:00
a0b96aa06c chore: update dependencies 2020-10-26 09:26:25 +01:00
2dc16e8ea8 feat(ui): display table properties tab 2020-10-24 14:47:35 +02:00
ee183886f6 fix(mysql): error getting foreign key list 2020-10-23 16:21:36 +02:00
cef6f681c8 refactor(ui): improve scss 2020-10-23 13:58:47 +02:00
ea9b489f5f fix: duplicate header fields on join result tables 2020-10-21 14:58:22 +02:00
1658432fd3 feat: support to aliased tables 2020-10-20 13:30:36 +02:00
a8cd17748f fix: wrong result fields type and order with some queries 2020-10-20 13:12:12 +02:00
56 changed files with 4061 additions and 406 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text eol=lf

View File

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

View File

@@ -2,6 +2,68 @@
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)

View File

@@ -4,14 +4,16 @@
# Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/estarium/antares) [![Build Status](https://travis-ci.com/EStarium/antares.svg?branch=master)](https://travis-ci.com/EStarium/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/estarium/antares/total) ![GitHub](https://img.shields.io/github/license/estarium/antares)
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares)
Antares 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 (yes, i'm a lone dev), hoping to provide all essential features as soon as possible.
**At the moment this application is an alpha, it lacks many features** and supports only MySQL.
Most of its current features might be enough for basic MySQL use, so give it a chance and send me your feedback, I would really appreciate it.
I'm actively working on it (yes, i'm a lone dev), hoping to provide cool features and fixes as soon as possible.
🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/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.
@@ -23,15 +25,15 @@ An application created with minimalism and semplicity in mind, with features in
## How to contribute
- [Translate Antares](https://github.com/EStarium/antares/wiki/Translate-Antares)
- [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
## Current main features
- Multiple database connections at same time.
- Table's content management (add/edit/delete).
- Database management (add/edit/delete).
- Full tables management, including indexes and foreign keys.
- Run queries on multiple tabs.
- Query suggestions.
- Database management (add/edit/delete).
- Native dark theme.
- Multi language.
- Auto updates.
@@ -40,13 +42,14 @@ An application created with minimalism and semplicity in mind, with features in
This is a roadmap with major features will come in near future.
- Improvements of query editor area.
- Tables management (add/edit/delete).
- Stored procedures, views, schedulers and triggers support.
- Users management (add/edit/delete).
- Stored procedures, views, schedulers and trigger support.
- 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.
@@ -54,7 +57,6 @@ This is a roadmap with major features will come in near future.
- Query logs console.
- Fake data filler.
- Import/export and migration.
- SSL and SSH tunnel support.
- Themes.
## Currently supported
@@ -84,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
View File

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

View File

@@ -1,10 +1,10 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.0.8",
"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,43 +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.3",
"mysql": "^2.18.1",
"pg": "^8.4.0",
"pg": "^8.5.1",
"source-map-support": "^0.5.16",
"spectre.css": "^0.5.9",
"vue-i18n": "^8.21.0",
"vue-i18n": "^8.22.2",
"vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.1",
"vuex": "^3.5.1",
"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.3",
"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.10.0",
"eslint-config-standard": "^14.1.1",
"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": "^7.0.1",
"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.2",
"stylelint": "^13.8.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"vue": "^2.6.12",

View 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
}
]
}
];

View File

@@ -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'];

View File

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

View File

@@ -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;
});
}

View File

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

View File

@@ -14,14 +14,29 @@ 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 };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-table-indexes', async (event, params) => {
try {
const result = await connections[params.uid].getTableIndexes(params);
return { status: 'success', response: result };
}
@@ -78,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 };
@@ -134,7 +156,7 @@ export default (connections) => {
}
});
ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, description }) => {
ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => {
try {
const query = connections[uid]
.select(`${column} AS foreignColumn`)
@@ -153,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() };
}
});
};

View File

@@ -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);
}
}

View File

@@ -177,7 +177,7 @@ export class MySQLClient extends AntaresCore {
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE,
type: field.DATA_TYPE.toUpperCase(),
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION,
@@ -192,11 +192,36 @@ export class MySQLClient extends AntaresCore {
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
@@ -212,17 +237,27 @@ export class MySQLClient extends AntaresCore {
.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,
column: field.COLUMN_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,
refColumn: field.REFERENCED_COLUMN_NAME
refField: field.REFERENCED_COLUMN_NAME,
onUpdate: extra.UPDATE_RULE,
onDelete: extra.DELETE_RULE
};
});
}
@@ -266,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
@@ -329,43 +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,
schema: field.db,
table: field.orgTable,
type: 'varchar'
};
}) : [];
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: remappedFields
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;

View File

@@ -17,6 +17,7 @@
<TheNotificationsBoard />
<ModalNewConnection v-if="isNewConnModal" />
<ModalSettings v-if="isSettingModal" />
<ModalDiscardChanges v-if="isUnsavedDiscardModal" />
</div>
</div>
</template>
@@ -35,7 +36,8 @@ export default {
TheAppWelcome: () => import(/* webpackChunkName: "TheAppWelcome" */'@/components/TheAppWelcome'),
Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'),
ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'),
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'),
ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges')
},
data () {
return {};
@@ -46,7 +48,8 @@ export default {
isNewConnModal: 'application/isNewModal',
isEditModal: 'application/isEditModal',
isSettingModal: 'application/isSettingModal',
connections: 'connections/getConnections'
connections: 'connections/getConnections',
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal'
})
},
mounted () {

View File

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

View File

@@ -86,9 +86,8 @@ export default {
.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;
@@ -103,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;
}
}
}
}

View File

@@ -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
});

View File

@@ -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');

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

View File

@@ -20,6 +20,7 @@
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="localConnection.name"
class="form-input"
type="text"
@@ -172,6 +173,10 @@ export default {
created () {
this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);

View File

@@ -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"
@@ -114,6 +118,10 @@ export default {
};
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);

View File

@@ -20,6 +20,7 @@
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
@@ -182,6 +183,10 @@ export default {
},
created () {
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);

View File

@@ -19,6 +19,7 @@
</div>
<div class="col-9">
<input
ref="firstInput"
v-model="database.name"
class="form-input"
type="text"
@@ -89,6 +90,9 @@ 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);

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

View File

@@ -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: {
@@ -197,6 +196,12 @@ 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);
@@ -301,7 +306,7 @@ 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();

View File

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

View File

@@ -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();

View File

@@ -71,7 +71,7 @@ export default {
}),
confirmDeleteConnection () {
this.deleteConnection(this.contextConnection);
this.$emit('close-context');
this.closeContext();
},
showEditModal () {
this.isEditModal = true;
@@ -85,6 +85,7 @@ export default {
},
hideConfirmModal () {
this.isConfirmModal = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');

View File

@@ -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')">

View File

@@ -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,7 +40,7 @@
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 class="tab-link">
<i class="mdi mdi-18px mdi-code-tags mr-1" />
@@ -56,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"
@@ -83,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',
@@ -106,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');
@@ -137,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;
@@ -203,6 +234,11 @@ export default {
&.active a {
opacity: 1;
}
.workspace-tools-link {
padding-bottom: 0;
padding-top: 0.3rem;
}
}
}
}

View File

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

View File

@@ -18,7 +18,7 @@
:key="table.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.name}"
@click="changeBreadcrumbs({schema: database.name, table: table.name})"
@click="setBreadcrumbs({schema: database.name, table: table.name})"
@contextmenu.prevent="showTableContext($event, table.name)"
>
<a class="table-name">
@@ -70,12 +70,16 @@ export default {
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);
}
}
};

View File

@@ -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');
}

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

View File

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

View File

@@ -0,0 +1,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>

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

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

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

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

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

View File

@@ -1,7 +1,11 @@
<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
@@ -16,10 +20,10 @@
</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
@@ -50,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';
@@ -74,8 +77,8 @@ export default {
lastQuery: '',
isQuering: false,
results: [],
resultsCount: false,
affectedCount: false
resultsCount: 0,
affectedCount: 0
};
},
computed: {
@@ -94,29 +97,13 @@ export default {
},
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.table) cachedTable = field.table;// Needed for some queries on information_schema
return {
table: field.table || cachedTable,
schema: field.schema || 'INFORMATION_SCHEMA'
};
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
}
return [];
},
async runQuery (query) {
if (!query || this.isQuering) return;
this.isQuering = true;
this.clearTabData();
this.$refs.queryTable.resetSort();
try { // Query Data
const params = {
@@ -129,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.name);
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.length >= result.fields.length ? fields : result.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 });
@@ -225,14 +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) {
e.stopPropagation();
if (e.key === 'F9')
this.runQuery(this.query);
if (this.isSelected) {
e.stopPropagation();
if (e.key === 'F9')
this.runQuery(this.query);
}
}
}
};

View File

@@ -73,7 +73,7 @@
@contextmenu="contextMenu"
/>
</template>
</basevirtualscroll>
</BaseVirtualScroll>
</div>
</div>
</template>
@@ -115,7 +115,6 @@ export default {
},
computed: {
...mapGetters({
getWorkspaceTab: 'workspaces/getWorkspaceTab',
getWorkspace: 'workspaces/getWorkspace'
}),
workspaceSchema () {
@@ -124,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];
@@ -142,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: {
@@ -225,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) {
@@ -268,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),
@@ -338,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 = '';

View File

@@ -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;
},

View File

@@ -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,7 @@ 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;
@@ -273,7 +277,7 @@ export default {
if (field)
type = field.type;
return type;
return type.toLowerCase();
},
getFieldPrecision (cKey) {
let length = 0;
@@ -284,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' : '';
@@ -301,7 +316,7 @@ export default {
editON (event, content, field) {
if (!this.isEditable) return;
const type = this.getFieldType(field);
const type = this.getFieldType(field).toUpperCase(); ;
this.originalContent = content;
this.editingType = type;
this.editingField = field;
@@ -341,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 () {
@@ -410,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);
}
}
};

View File

@@ -1,5 +1,5 @@
<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">
@@ -42,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>
@@ -60,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"
@@ -94,12 +100,11 @@ export default {
tabUid: 'data',
isQuering: false,
results: [],
fields: [],
keyUsage: [],
lastTable: null,
isAddModal: false,
autorefreshTimer: 0,
refreshInterval: null
refreshInterval: null,
sortParams: {}
};
},
computed: {
@@ -111,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) {
@@ -137,55 +158,23 @@ export default {
},
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;
const fieldsArr = [];
const keysArr = [];
// if table changes clear cached values
if (this.lastTable !== this.table) {
if (this.lastTable !== this.table)
this.results = [];
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
}
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 { // 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 });
}
try { // Table data
const { status, response } = await Tables.getTableData(params);
@@ -198,16 +187,17 @@ export default {
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;
@@ -216,9 +206,11 @@ export default {
this.isAddModal = false;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'F5')
this.reloadTable();
if (this.isSelected) {
e.stopPropagation();
if (e.key === 'F5')
this.reloadTable();
}
},
setRefreshInterval () {
if (this.refreshInterval)
@@ -234,38 +226,3 @@ export default {
}
};
</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>

View File

@@ -42,7 +42,27 @@ module.exports = {
connecting: 'Connecting',
name: 'Name',
collation: 'Collation',
autoRefresh: 'Auto-refresh'
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!',
@@ -79,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: {

View File

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

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

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

View File

@@ -4,15 +4,22 @@
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;
}
}

View File

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

View File

@@ -64,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;
@@ -148,6 +180,12 @@ body {
.form-select {
cursor: pointer;
&.small-select {
height: 1rem;
font-size: 0.7rem;
padding: 1px 0.4rem 0;
}
}
.form-select,
@@ -159,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;
}
@@ -191,3 +234,7 @@ body {
visibility: hidden;
}
}
.empty {
color: $body-font-color;
}

View File

@@ -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);
}
}
};