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

Compare commits

..

123 Commits

Author SHA1 Message Date
3b4f1475df chore(release): 0.0.12 2020-12-24 10:58:03 +01:00
155154b43d feat: option to toggle editor auto completion 2020-12-24 10:40:22 +01:00
a95b8d188c feat: option to change editor theme 2020-12-23 18:07:50 +01:00
cb1fce6f99 feat: query editor auto-completer for tables and columns 2020-12-22 22:31:31 +01:00
0014f48079 refactor: migrated to ace from monaco-editor 2020-12-21 11:06:41 +01:00
fc35f271d7 feat: better security connections credentials storage 2020-12-18 18:44:32 +01:00
65ad0e954d ci: update travis configuration 2020-12-18 17:38:06 +01:00
8cafade8b1 chore(release): 0.0.11 2020-12-15 17:24:04 +01:00
d13b708377 chore: update REDME.md 2020-12-15 17:23:24 +01:00
206597e5b8 feat: foreign keys management 2020-12-15 17:08:36 +01:00
c5458159d1 fix: unable to switch tabs when no table selected 2020-12-11 18:22:07 +01:00
1476e899d1 feat: auto focus on first input in modals 2020-12-11 18:09:17 +01:00
797ab70e7c chore: update links 2020-12-11 16:05:32 +01:00
f81312aeb0 feat: query tabs auto focus 2020-12-11 15:55:18 +01:00
3ed5ea023e fix: some properties do not reset after fields changes 2020-12-11 12:57:24 +01:00
9291a7a7b4 fix: file field editor not show 2020-12-10 15:15:32 +01:00
5cfdc9b92d fix: wrong field type detection 2020-12-09 18:22:46 +01:00
15b08d7ea8 fix: data tab sort not maintained at refresh 2020-12-08 18:41:08 +01:00
acebe435ff fix: improved changes dedection in props tab 2020-12-07 19:11:29 +01:00
5712b80022 feat: improved data table sorts 2020-12-07 17:51:48 +01:00
d38583262e fix: deletion of rows with non-numeric ID 2020-12-07 15:07:59 +01:00
e0e2131981 chore: update README.md 2020-12-04 11:43:27 +01:00
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
580105e9f3 chore(release): 0.0.8 2020-10-18 10:29:37 +02:00
0626f6f775 refactor(render): improved buttons style 2020-10-18 10:27:02 +02:00
12f5e479f3 chore: update README.md 2020-10-17 17:01:38 +02:00
04804b07c7 feat(render): field type and length on table header mouse hover 2020-10-17 10:12:40 +02:00
053418ee90 refactor(mysql): moved specific queries inside MySQLClient class 2020-10-16 17:26:47 +02:00
426628f268 feat: pie chart with table size in database explore bar 2020-10-15 17:22:19 +02:00
d4ecaf65e5 fix: context menu outside window when near bottom or right edge 2020-10-14 19:32:36 +02:00
27d114beef refactor: remap of procedures, triggers and schedulers data objects 2020-10-14 19:00:13 +02:00
936de04cd3 refactor: remap of table data object 2020-10-12 18:45:15 +02:00
b7c779eef6 fix: disable cell editor for not editable results 2020-10-10 16:54:00 +02:00
d560c384f5 fix: missing header for some query results 2020-10-09 22:44:05 +02:00
9ecd88870d feat: data table autorefresh, closes #36 2020-10-08 18:51:08 +02:00
07d1e82325 perf: improved refresh of data tables 2020-10-07 20:42:04 +02:00
ce25cd0a31 fix: no connection passed to connection's edit modal 2020-10-05 09:21:33 +02:00
319d9beef1 Merge pull request #35 from EStarium/dependabot/npm_and_yarn/eslint-plugin-vue-7.0.1
build(deps-dev): bump eslint-plugin-vue from 6.2.2 to 7.0.1
2020-10-05 08:58:15 +02:00
dependabot[bot]
4272efe73b build(deps-dev): bump eslint-plugin-vue from 6.2.2 to 7.0.1
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 6.2.2 to 7.0.1.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v6.2.2...v7.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-05 05:42:39 +00:00
ed5cf0a8e4 refactor: improvements to edit connection modal 2020-10-04 18:32:54 +02:00
c70e5b422c fix: missing connection name when "ask for crendential" selected 2020-10-04 18:31:40 +02:00
0bf2c8dc9d feat: query and data tabs keyboard shortcuts (F5, F9) 2020-10-04 17:32:15 +02:00
d563cec70d feat: close modals pressing ESC 2020-10-04 17:21:21 +02:00
6ee4ef4b8b chore: improved changelog 2020-10-03 12:20:09 +02:00
5fc4df426c chore(release): 0.0.7 2020-10-03 12:13:17 +02:00
799a5fef5b build: reduced webpack console output 2020-10-03 12:12:41 +02:00
54717e1f6a feat: edit database collation 2020-10-03 12:11:42 +02:00
4288a1fd33 feat: databases deletion 2020-10-01 15:08:35 +02:00
52449e0420 refactor: enhanced automatic schema selection 2020-09-29 16:43:20 +02:00
96f38297c1 chore: update README.md 2020-09-28 10:14:12 +02:00
3e737cba62 fix: empty databases not shown in explore bar 2020-09-27 19:06:13 +02:00
3d0a83f2cf feat: database creation 2020-09-25 12:39:58 +02:00
c1cdd03938 refactor: simplified and improved project structure 2020-09-24 13:09:01 +02:00
437e41bff0 Merge pull request #34 from EStarium/dependabot/npm_and_yarn/vuex-persist-3.1.0
build(deps): bump vuex-persist from 2.3.0 to 3.1.0
2020-09-21 08:02:46 +02:00
dependabot[bot]
4516783b13 build(deps): bump vuex-persist from 2.3.0 to 3.1.0
Bumps [vuex-persist](https://github.com/championswimmer/vuex-persist) from 2.3.0 to 3.1.0.
- [Release notes](https://github.com/championswimmer/vuex-persist/releases)
- [Changelog](https://github.com/championswimmer/vuex-persist/blob/master/CHANGELOG.md)
- [Commits](https://github.com/championswimmer/vuex-persist/compare/v2.3.0...v3.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-21 05:45:49 +00:00
43c7072c1c fix: unable to obtain fields informations for some queries 2020-09-20 16:03:03 +02:00
530d1bd43f fix: missing schema when queryng INFORMATION_SCHEMA 2020-09-19 18:10:57 +02:00
530907d097 fix: several fix on data and query tabs 2020-09-18 12:54:02 +02:00
ac4941fa5e style: line break from CRLF to LF 2020-09-17 18:15:48 +02:00
5334a44271 refactor: improved structure of connection core 2020-09-17 17:58:12 +02:00
4991291c2c chore: update app screenshot 2020-09-17 12:34:57 +02:00
2554444322 feat: field comment on mouse over a table field name 2020-09-17 11:13:00 +02:00
cff4f537de chore: improved app icon resolution 2020-09-15 15:07:56 +02:00
12fbe8c1a0 fix: prevent multiple app instances 2020-09-15 14:44:29 +02:00
10b426b90b fix: glitch on table data tab 2020-09-14 12:49:09 +02:00
b29e07c3b7 fix: wrong italian translation 2020-09-14 12:47:01 +02:00
ffef312b44 Merge pull request #33 from ReverbOD/master
feat: update italian translation
2020-09-14 12:38:33 +02:00
Giuseppe Gigliotti
264829bec0 Update it-IT.js 2020-09-14 12:31:42 +02:00
Giuseppe Gigliotti
89c3dc9fed feat: update italian translation 2020-09-14 12:28:22 +02:00
Giuseppe Gigliotti
fe3d741601 feat: Update italian translation 2020-09-14 11:45:21 +02:00
1b04b216b2 fix: cell update soft reload doesn't apply changes 2020-09-14 11:08:11 +02:00
78965d23e3 fix: value overridden when join tables with fields with same name 2020-09-13 18:25:28 +02:00
9b76c8eae0 Merge pull request #32 from hongkfui/master
Spanish translation added
2020-09-12 15:30:18 +02:00
hongkfui
c94ae8c9bc Spanish lang added 2020-09-12 12:34:28 +02:00
ad0bad8486 fix: wrong field names when join tables 2020-09-11 18:01:07 +02:00
8e71f42a28 fix: wrong schema fetching table fields and key usage 2020-09-10 12:39:23 +02:00
967a0aa6e3 chore: update issue templates 2020-09-09 11:43:06 +02:00
ddc7d1ea26 ci: create codeql-analysis.yml 2020-09-09 10:48:21 +02:00
4684b4114b fix: wrong table and schema when more than one query in a tab 2020-09-08 11:47:01 +02:00
3e08ba221d build: local vue-template-compiler 2020-09-06 16:47:51 +02:00
1d87ca959f refactor: adaptation of row deletion and modification functions due last commits 2020-09-06 10:35:32 +02:00
023c6a633a fix: unable to obtain keyUsage informations when adding new row 2020-09-06 10:09:05 +02:00
48f77bae01 feat: support to multiple queries in the same tab 2020-09-06 08:41:57 +02:00
3385744260 docs: update README.md 2020-09-03 15:19:31 +02:00
86aec4f5e4 fix: lack of loading progressbar when an update is available 2020-09-03 15:12:30 +02:00
ba73d677b5 chore: 2020-09-03 13:52:43 +02:00
97 changed files with 8911 additions and 7311 deletions

View File

@@ -21,7 +21,7 @@
],
"linebreak-style": [
"error",
"windows"
"unix"
],
"brace-style": [
"error",

6
.gitattributes vendored Normal file
View File

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

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

57
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 15 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,9 +1,6 @@
language: node_js
node_js: 12
before_install:
- npm install
cache:
directories:
- node_modules
@@ -20,19 +17,36 @@ env:
jobs:
include:
- stage: Test
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script:
- npm test
- stage: Deploy Linux & Windows
if: tag IS present
os: linux
services: docker
before_install:
- sudo apt-get install libsecret-1-dev
- npm install
script:
- docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always"
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
- stage: Deploy Mac
if: tag IS present
os: osx
before_install:
- npm install
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,24 +2,147 @@
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.12](https://github.com/Fabio286/antares/compare/v0.0.11...v0.0.12) (2020-12-24)
### Features
* better security connections credentials storage ([fc35f27](https://github.com/Fabio286/antares/commit/fc35f271d7fe384cd786ce33547c0ef17135ddd8))
* option to change editor theme ([a95b8d1](https://github.com/Fabio286/antares/commit/a95b8d188cfcc8f563ad73b4f0b676d068775d36))
* option to toggle editor auto completion ([155154b](https://github.com/Fabio286/antares/commit/155154b43d0cd02ae875ded3ce865a37a999da5c))
* query editor auto-completer for tables and columns ([cb1fce6](https://github.com/Fabio286/antares/commit/cb1fce6f998ea7332886820910e245ab19416a9d))
### [0.0.11](https://github.com/Fabio286/antares/compare/v0.0.10...v0.0.11) (2020-12-15)
### Features
* auto focus on first input in modals ([1476e89](https://github.com/Fabio286/antares/commit/1476e899d164562f12342ced8c76903b9bdcfa55))
* foreign keys management ([206597e](https://github.com/Fabio286/antares/commit/206597e5b891e13e6f7635075bd11599355ab778))
* improved data table sorts ([5712b80](https://github.com/Fabio286/antares/commit/5712b8002203b32027f0e820f98a61e2ec965e79))
* query tabs auto focus ([f81312a](https://github.com/Fabio286/antares/commit/f81312aeb02ef55affd2ae9e81a9b4cb4c2e6da2))
### Bug Fixes
* data tab sort not maintained at refresh ([15b08d7](https://github.com/Fabio286/antares/commit/15b08d7ea858cb28111c7b548af9a13b1bf0da91))
* deletion of rows with non-numeric ID ([d385832](https://github.com/Fabio286/antares/commit/d38583262e672a2b47c5ad0aca0f13c129830a7b))
* file field editor not show ([9291a7a](https://github.com/Fabio286/antares/commit/9291a7a7b41e7aeb9b65c7f32e496f523e482272))
* improved changes dedection in props tab ([acebe43](https://github.com/Fabio286/antares/commit/acebe435ff6fa1581692fbf7ee1bc23b334e3947))
* some properties do not reset after fields changes ([3ed5ea0](https://github.com/Fabio286/antares/commit/3ed5ea023e1852d724b2b59ab156f8722876f85b))
* unable to switch tabs when no table selected ([c545815](https://github.com/Fabio286/antares/commit/c5458159d1e30cecf6615086c074d98b4b599637))
* wrong field type detection ([5cfdc9b](https://github.com/Fabio286/antares/commit/5cfdc9b92d4b778a7863b02fd64e6445236c89bc))
### [0.0.10](https://github.com/Fabio286/antares/compare/v0.0.9...v0.0.10) (2020-12-04)
### Features
* approximate totals in table tata tab ([e95d29c](https://github.com/Fabio286/antares/commit/e95d29c7c37e24e7cc14b466f9b539fa667042c2))
* create new tables ([e6602d1](https://github.com/Fabio286/antares/commit/e6602d1bfa9ca10c6bb078ee80ddc94fb338763d))
* display all keys in properties tab ([27769f2](https://github.com/Fabio286/antares/commit/27769f204f731d20c7ba2f838c02b7c2f28fa0c3))
* drop and truncate tables ([a4122b4](https://github.com/Fabio286/antares/commit/a4122b4eaaa5b30d97ba5a93df8c9d21c30bc40b))
* index management ([41505bd](https://github.com/Fabio286/antares/commit/41505bde6547c0af3c3413248ad8a0d182838bb1))
* tables options edit ([0805b96](https://github.com/Fabio286/antares/commit/0805b96a75e439a7d65e8341ecc86fa938679a9f))
* unsaved changes reminder ([33d1fa2](https://github.com/Fabio286/antares/commit/33d1fa22905f477924292135b0dcfefe168ee641))
### Bug Fixes
* index deletion issue ([f8cf90a](https://github.com/Fabio286/antares/commit/f8cf90a89e7367c95e164b7dc669506df392b700))
* some problems with properties and data tabs when changing database from sidebar ([0fe7157](https://github.com/Fabio286/antares/commit/0fe71572a5e74c17a5c66237351bb0b02c33e824))
* sqlEscaper function wrong quotes conversion ([dfb24c6](https://github.com/Fabio286/antares/commit/dfb24c65f3c395d78d27a2f29e9aa8eeb427cff7))
### [0.0.9](https://github.com/EStarium/antares/compare/v0.0.8...v0.0.9) (2020-11-13)
### Features
* ability to edit table fields ([249926b](https://github.com/EStarium/antares/commit/249926b8e040d62d50244362b7f999f26337b93c))
* support to aliased tables ([1658432](https://github.com/EStarium/antares/commit/1658432fd30073ba3bffb39b5c4ca69194ae1330))
* table fields addition ([0765403](https://github.com/EStarium/antares/commit/07654039b6a99f3115c378b53d659593e5c81f35))
* table fields deletion ([242ddec](https://github.com/EStarium/antares/commit/242ddec744814d15657db1ca88b2d865045ea219))
* **ui:** display table properties tab ([2dc16e8](https://github.com/EStarium/antares/commit/2dc16e8ea8d6d9b79288335888e155ff180eebf5))
### Bug Fixes
* duplicate header fields on join result tables ([ea9b489](https://github.com/EStarium/antares/commit/ea9b489f5f45cffa4a7ac87873fff070205e88c4))
* F9 key shortcut refresh all query tabs instead of just selected one ([c9ba2e5](https://github.com/EStarium/antares/commit/c9ba2e5962eae1afc46daf35e55fb0ea5c3af5a4))
* issue with tabs horizontal scroll with wheel ([c393f86](https://github.com/EStarium/antares/commit/c393f86947d1f65f896cadaa39d53f13e0a1f4eb))
* zero fill field option was not saved ([3e5770f](https://github.com/EStarium/antares/commit/3e5770f7de51bdf2bc0f1f38c7ceb9ef0f4dcd00))
* **mysql:** error getting foreign key list ([ee18388](https://github.com/EStarium/antares/commit/ee183886f64947305cc4f0d38dbdf7919953ec01))
* wrong result fields type and order with some queries ([a8cd177](https://github.com/EStarium/antares/commit/a8cd17748f4ac7d75092f65ae7ca5f96a8a9e8c5))
### [0.0.8](https://github.com/EStarium/antares/compare/v0.0.7...v0.0.8) (2020-10-18)
### Features
* **render:** field type and length on table header mouse hover ([04804b0](https://github.com/EStarium/antares/commit/04804b07c71cec271c31ace13bd41b2c7415e892))
* close modals pressing ESC ([d563cec](https://github.com/EStarium/antares/commit/d563cec70d996f66c4f724bba7de618fc8678e66))
* data table autorefresh, closes [#36](https://github.com/EStarium/antares/issues/36) ([9ecd888](https://github.com/EStarium/antares/commit/9ecd88870d1fcf32bb2c970a1506206c477810a0))
* pie chart with table size in database explore bar ([426628f](https://github.com/EStarium/antares/commit/426628f268c77496a13b3498f03fd7b11fee299a))
* query and data tabs keyboard shortcuts (F5, F9) ([0bf2c8d](https://github.com/EStarium/antares/commit/0bf2c8dc9dd9bdf7a8f48bed61eed7f1f1aacf71))
### Bug Fixes
* context menu outside window when near bottom or right edge ([d4ecaf6](https://github.com/EStarium/antares/commit/d4ecaf65e56044170139bac61c3ee69efc35a8f0))
* disable cell editor for not editable results ([b7c779e](https://github.com/EStarium/antares/commit/b7c779eef63c257c166e7128ea643bdd6142aa88))
* missing connection name when "ask for crendential" selected ([c70e5b4](https://github.com/EStarium/antares/commit/c70e5b422c3534e92a64a0b534eb58663621489c))
* missing header for some query results ([d560c38](https://github.com/EStarium/antares/commit/d560c384f5aed58ea975935975843c3b9061dd85))
* no connection passed to connection's edit modal ([ce25cd0](https://github.com/EStarium/antares/commit/ce25cd0a3130db486ea4da24dd393d45c2ef9e0d))
### [0.0.7](https://github.com/EStarium/antares/compare/v0.0.6...v0.0.7) (2020-10-03)
### Features
* Database creation ([3d0a83f](https://github.com/EStarium/antares/commit/3d0a83f2cf68c4dd412fd7679c39d63f081b7c19))
* Database deletion ([4288a1f](https://github.com/EStarium/antares/commit/4288a1fd331f4a28de2e756f898d208a6a6599c4))
* Edit database collation ([54717e1](https://github.com/EStarium/antares/commit/54717e1f6a36ec0b3dd096d0e1e747512f6dda09))
* Field comment on mouse over a table field name ([2554444](https://github.com/EStarium/antares/commit/2554444322b59a6b1ab3ff05ccf8604bf6f8c8b8))
* Support to multiple queries in the same tab ([48f77ba](https://github.com/EStarium/antares/commit/48f77bae01efbff40bd0f5ce8c66e2619f44bf3a))
* Update italian translation ([89c3dc9](https://github.com/EStarium/antares/commit/89c3dc9fede63c77eb22b48df1a375ea44830306))
* **Spanish translation** thanks to
[hongkfui](https://github.com/hongkfui) ([#32](https://github.com/EStarium/antares/pull/32))
### Bug Fixes
* Cell update soft reload doesn't apply changes ([1b04b21](https://github.com/EStarium/antares/commit/1b04b216b21b697e47062a9366bc1b6a040a1a72))
* Empty databases not shown in explore bar ([3e737cb](https://github.com/EStarium/antares/commit/3e737cba62f795f225e944939c6bff04b27fa3d4))
* Glitch on table data tab ([10b426b](https://github.com/EStarium/antares/commit/10b426b90b6b9461cfffce3026c982463f6e0599))
* Lack of loading progressbar when an update is available ([86aec4f](https://github.com/EStarium/antares/commit/86aec4f5e41c059e88066a01f0d85155de99a5ee))
* Missing schema when queryng INFORMATION_SCHEMA ([530d1bd](https://github.com/EStarium/antares/commit/530d1bd43fa95de05f594b9b5cae2f4b397f96e0))
* Prevent multiple app instances ([12fbe8c](https://github.com/EStarium/antares/commit/12fbe8c1a03259648554f2a5c69b5abbedc18a48))
* Several fix on data and query tabs ([530907d](https://github.com/EStarium/antares/commit/530907d097ac4d995e1bfcb02e6c890fd6007e21))
* Unable to obtain fields informations for some queries ([43c7072](https://github.com/EStarium/antares/commit/43c7072c1c83a2455ae48a37be69b444b3eb6560))
* Unable to obtain keyUsage informations when adding new row ([023c6a6](https://github.com/EStarium/antares/commit/023c6a633a7f268b1a97b748ad08d2416cc30ffe))
* Value overridden when join tables with fields with same name ([78965d2](https://github.com/EStarium/antares/commit/78965d23e3efb7d8d6d110d79142966e57200757))
* Wrong field names when join tables ([ad0bad8](https://github.com/EStarium/antares/commit/ad0bad8486c3d67ec14ec1aed3d8aff6cce9df87))
* Wrong italian translation ([b29e07c](https://github.com/EStarium/antares/commit/b29e07c3b722aec7e78f3cef2e357a53cbcac474))
* Wrong schema fetching table fields and key usage ([8e71f42](https://github.com/EStarium/antares/commit/8e71f42a28060fdfeeb81502b0759d0d11f5bcfd))
* Wrong table and schema when more than one query in a tab ([4684b41](https://github.com/EStarium/antares/commit/4684b4114b9c9c253120292d7d164d7676011f86))
### [0.0.6](https://github.com/EStarium/antares/compare/v0.0.5...v0.0.6) (2020-09-03)
### Features
* aliases support ([264de9c](https://github.com/EStarium/antares/commit/264de9c5686fb3a2ef22d96171f45b915ba1b34b))
* middle click to close tabs ([256ec76](https://github.com/EStarium/antares/commit/256ec765883fcf247355190827e943c76e95f13b))
* monaco-editor as query editor ([196a3e0](https://github.com/EStarium/antares/commit/196a3e0185a3d68b7c4ade8dbf187d2b216cc00b))
* sql suggestions in query editor ([8dc74ef](https://github.com/EStarium/antares/commit/8dc74ef2c335e8ae4a69f5d2651df65939139b1b))
* support to multiple query tabs ([d7ed00f](https://github.com/EStarium/antares/commit/d7ed00f4a3613da9015c9fc48c4d8062d292e416))
* tabs horizontal scroll with mouse wheel ([3a6ea76](https://github.com/EStarium/antares/commit/3a6ea76b93682ebd50908df7368c62c2c1e27958))
* Aliases support ([264de9c](https://github.com/EStarium/antares/commit/264de9c5686fb3a2ef22d96171f45b915ba1b34b))
* Middle click to close tabs ([256ec76](https://github.com/EStarium/antares/commit/256ec765883fcf247355190827e943c76e95f13b))
* Monaco-editor as query editor ([196a3e0](https://github.com/EStarium/antares/commit/196a3e0185a3d68b7c4ade8dbf187d2b216cc00b))
* Sql suggestions in query editor ([8dc74ef](https://github.com/EStarium/antares/commit/8dc74ef2c335e8ae4a69f5d2651df65939139b1b))
* Support to multiple query tabs ([d7ed00f](https://github.com/EStarium/antares/commit/d7ed00f4a3613da9015c9fc48c4d8062d292e416))
* Tabs horizontal scroll with mouse wheel ([3a6ea76](https://github.com/EStarium/antares/commit/3a6ea76b93682ebd50908df7368c62c2c1e27958))
* **Arabic translation** thanks to [Mohd-PH](https://github.com/Mohd-PH) ([#29](https://github.com/EStarium/antares/pull/29))
### Bug Fixes
* error when launching queries without a result from query tabs ([a1a6f51](https://github.com/EStarium/antares/commit/a1a6f51f2fba5140f5e3bd9cd6557c8a13dfaa2c))
* field name displayed instead of alias ([801a0de](https://github.com/EStarium/antares/commit/801a0de1865dea2a59ff057b7c2cc988cc9c87ed))
* wrong table height calc in some cases ([fd6d517](https://github.com/EStarium/antares/commit/fd6d5177efb6161aab01f9e108eda60df6c7d8c4))
* Error when launching queries without a result from query tabs ([a1a6f51](https://github.com/EStarium/antares/commit/a1a6f51f2fba5140f5e3bd9cd6557c8a13dfaa2c))
* Field name displayed instead of alias ([801a0de](https://github.com/EStarium/antares/commit/801a0de1865dea2a59ff057b7c2cc988cc9c87ed))
* Wrong table height calc in some cases ([fd6d517](https://github.com/EStarium/antares/commit/fd6d5177efb6161aab01f9e108eda60df6c7d8c4))
### [0.0.5](https://github.com/EStarium/antares/compare/v0.0.4...v0.0.5) (2020-08-17)

View File

@@ -4,37 +4,54 @@
# 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, 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), and stay tuned for updates.
🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases).
👁 To stay tuned for new releases watch this repo on **Release only** channel.
🌟 Don't forget to **leave a star** if you appreciate this project.
## Philosophy
Why am I developing an SQL client when there are a lot of them on the market?
The main goal is to develop a totally free, cross platform and open source alternative, empowered by JavaScript's ecosystem.
An application created with minimalism and semplicity in mind, with features in the righ places, not hundreds of tiny buttons or submenu.
The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem.
An application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons or submenu.
## How to contribute
- [Translate Antares](https://github.com/EStarium/antares/wiki/Translate-Antares)
- [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
## Roadmap
## Current main features
- Multiple database connections at same time.
- Database management (add/edit/delete).
- Full tables management, including indexes and foreign keys.
- Run queries on multiple tabs.
- Query suggestions and auto complete.
- Native dark theme.
- Multi language.
- Secure password storage.
- Auto updates.
## Coming soon
This is a roadmap with major features will come in near future.
- Improvements of query editor area.
- Multiple query tabs.
- Tables management (add/edit/delete).
- Stored procedures, views, schedulers and trigger support.
- Database tools.
- Context menu shortcuts.
- Keyboard shortcuts.
- More secure password storage.
- Stored procedures, views, schedulers and triggers support.
- Users management (add/edit/delete).
- Database tools (variables, process list...).
- SSL and SSH tunnel support.
- Support for other databases.
- UI/UX improvements.
- Query history.
- More context menu shortcuts.
- More keyboard shortcuts.
- Query logs console.
- Fake data filler.
- Import/export and migration.
@@ -45,7 +62,7 @@ This is a roadmap with major features will come in near future.
### Databases
- [x] MySQL/MariaDB
- [ ] PostrgreSQL
- [ ] PostgreSQL
- [ ] MSSQL
- [ ] SQLite
- [ ] OracleDB
@@ -53,7 +70,7 @@ This is a roadmap with major features will come in near future.
### Operating Systems
#### • x86
#### • x64
- [x] Windows
- [x] Linux
@@ -67,5 +84,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)
[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)

BIN
build/icon.icns Normal file

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 260 KiB

5
jsconfig.json Normal file
View File

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

View File

@@ -1,21 +1,23 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.0.6",
"version": "0.0.12",
"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",
"build": "cross-env NODE_ENV=production npm run compile && electron-builder",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"lint": "eslint ."
"test": "npm run lint",
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix"
},
"author": "Fabio Di Stasio <fabio286@gmail.com>",
"build": {
"appId": "com.estarium.antares",
"appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"dmg": {
"contents": [
@@ -45,47 +47,47 @@
}
},
"dependencies": {
"@mdi/font": "^5.5.55",
"electron-log": "^4.2.4",
"electron-updater": "^4.3.4",
"@mdi/font": "^5.8.55",
"ace-builds": "^1.4.12",
"electron-log": "^4.3.0",
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5",
"keytar": "^7.3.0",
"lodash": "^4.17.20",
"moment": "^2.27.0",
"monaco-editor": "^0.20.0",
"mssql": "^6.2.1",
"moment": "^2.29.1",
"mssql": "^6.2.3",
"mysql": "^2.18.1",
"pg": "^8.3.2",
"pg": "^8.5.1",
"source-map-support": "^0.5.16",
"spectre.css": "^0.5.9",
"vue-click-outside": "^1.1.0",
"vue-i18n": "^8.21.0",
"vue-i18n": "^8.22.2",
"vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.0",
"vuex": "^3.5.1",
"vuex-persist": "^2.2.0"
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0",
"vuex-persist": "^3.1.3"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.2",
"electron": "^10.1.0",
"electron-builder": "^22.8.0",
"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.7.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.22.0",
"eslint": "^7.14.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"monaco-editor-webpack-plugin": "^1.9.0",
"node-sass": "^4.14.1",
"sass-loader": "^10.0.1",
"eslint-plugin-vue": "^7.1.0",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.0",
"standard-version": "^9.0.0",
"stylelint": "^13.6.1",
"stylelint": "^13.8.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"vue": "^2.6.11",
"webpack": "^4.44.1"
"vue": "^2.6.12",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.2"
}
}

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

@@ -2,10 +2,14 @@
import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path';
import crypto from 'crypto';
import { format as formatUrl } from 'url';
import keytar from 'keytar';
import ipcHandlers from './ipc-handlers';
const isDevelopment = process.env.NODE_ENV !== 'production';
const gotTheLock = app.requestSingleInstanceLock();
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
// global reference to mainWindow (necessary to prevent window from being garbage collected)
@@ -67,23 +71,34 @@ async function createMainWindow () {
return window;
};
// Initialize ipcHandlers
ipcHandlers();
if (!gotTheLock)
app.quit();
else {
// Initialize ipcHandlers
ipcHandlers();
// quit application when all windows are closed
app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits
if (process.platform !== 'darwin')
app.quit();
});
// quit application when all windows are closed
app.on('window-all-closed', () => {
// on macOS it is common for applications to stay open until the user explicitly quits
if (process.platform !== 'darwin')
app.quit();
});
app.on('activate', () => {
// on macOS it is common to re-create a window even after all windows have been closed
if (mainWindow === null)
mainWindow = createMainWindow();
});
// create main BrowserWindow when electron is ready
app.on('ready', async () => {
let key = await keytar.getPassword('antares', 'user');
if (!key) {
key = crypto.randomBytes(16).toString('hex');
keytar.setPassword('antares', 'user', key);
}
app.on('activate', () => {
// on macOS it is common to re-create a window even after all windows have been closed
if (mainWindow === null)
mainWindow = createMainWindow();
});
// create main BrowserWindow when electron is ready
app.on('ready', () => {
mainWindow = createMainWindow();
});
});
}

View File

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

View File

@@ -1,12 +1,10 @@
import { ipcMain } from 'electron';
import { AntaresConnector } from '../libs/AntaresConnector';
import InformationSchema from '../models/InformationSchema';
import Generic from '../models/Generic';
import { ClientsFactory } from '../libs/ClientsFactory';
export default connections => {
ipcMain.handle('test-connection', async (event, conn) => {
const Connection = new AntaresConnector({
const connection = ClientsFactory.getConnection({
client: conn.client,
params: {
host: conn.host,
@@ -16,10 +14,11 @@ export default connections => {
}
});
await Connection.connect();
await connection.connect();
try {
await InformationSchema.testConnection(Connection);
await connection.select('1+1').run();
connection.destroy();
return { status: 'success' };
}
@@ -33,7 +32,7 @@ export default connections => {
});
ipcMain.handle('connect', async (event, conn) => {
const Connection = new AntaresConnector({
const connection = ClientsFactory.getConnection({
client: conn.client,
params: {
host: conn.host,
@@ -41,14 +40,16 @@ export default connections => {
user: conn.user,
password: conn.password
},
poolSize: 3
poolSize: 1
});
try {
await Connection.connect();
await connection.connect();
const structure = await connection.getStructure();
connections[conn.uid] = connection;
const { rows: structure } = await InformationSchema.getStructure(Connection);
connections[conn.uid] = Connection;
return { status: 'success', response: structure };
}
catch (err) {
@@ -60,25 +61,4 @@ export default connections => {
connections[uid].destroy();
delete connections[uid];
});
ipcMain.handle('refresh', async (event, uid) => {
try {
const { rows: structure } = await InformationSchema.getStructure(connections[uid]);
return { status: 'success', response: structure };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('raw-query', async (event, { uid, query, schema }) => {
if (!query) return;
try {
const result = await Generic.raw(connections[uid], query, schema);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -0,0 +1,121 @@
import { ipcMain } from 'electron';
export default connections => {
ipcMain.handle('create-database', async (event, params) => {
try {
const query = `CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`;
await connections[params.uid].raw(query);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('update-database', async (event, params) => {
try {
const query = `ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`;
await connections[params.uid].raw(query);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('delete-database', async (event, params) => {
try {
const query = `DROP DATABASE \`${params.database}\``;
await connections[params.uid].raw(query);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-database-collation', async (event, params) => { // TODO: move to mysql class
try {
const query = `SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`;
const collation = await connections[params.uid].raw(query);
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-structure', async (event, uid) => {
try {
const structure = await connections[uid].getStructure();
return { status: 'success', response: structure };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-collations', async (event, uid) => {
try {
const result = await connections[uid].getCollations();
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-variables', async (event, uid) => {
try {
const result = await connections[uid].getVariables();
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
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;
try {
await connections[uid].use(schema);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('raw-query', async (event, { uid, query, schema }) => {
if (!query) return;
try {
const result = await connections[uid].raw(query, { nest: true, details: true });
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -2,12 +2,14 @@ import connection from './connection';
import tables from './tables';
import updates from './updates';
import application from './application';
import database from './database';
const connections = {};
export default () => {
connection(connections);
tables(connections);
database(connections);
updates();
application();
};

View File

@@ -1,13 +1,12 @@
import { ipcMain } from 'electron';
import InformationSchema from '../models/InformationSchema';
import Tables from '../models/Tables';
// TODO: remap objects based on client
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes';
import fs from 'fs';
export default (connections) => {
ipcMain.handle('get-table-columns', async (event, { uid, schema, table }) => {
ipcMain.handle('get-table-columns', async (event, params) => {
try {
const result = await InformationSchema.getTableColumns(connections[uid], schema, table);// TODO: uniform column properties
const result = await connections[params.uid].getTableColumns(params);
return { status: 'success', response: result };
}
catch (err) {
@@ -15,9 +14,19 @@ 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 Tables.getTableData(connections[uid], schema, table);
const query = connections[uid]
.select('*')
.schema(schema)
.from(table)
.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) {
@@ -25,9 +34,21 @@ export default (connections) => {
}
});
ipcMain.handle('get-key-usage', async (event, { uid, schema, table }) => {
ipcMain.handle('get-table-indexes', async (event, params) => {
try {
const result = await InformationSchema.getKeyUsage(connections[uid], schema, table);
const result = await connections[params.uid].getTableIndexes(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-key-usage', async (event, params) => {
try {
const result = await connections[params.uid].getKeyUsage(params);
return { status: 'success', response: result };
}
catch (err) {
@@ -37,8 +58,34 @@ export default (connections) => {
ipcMain.handle('update-table-cell', async (event, params) => {
try {
const result = await Tables.updateTableCell(connections[params.uid], params);
return { status: 'success', response: result };
let escapedParam;
let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
if (NUMBER.includes(params.type))
escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type))
escapedParam = `"${sqlEscaper(params.content)}"`;
else if (BLOB.includes(params.type)) {
if (params.content) {
const fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
reload = true;
}
else
escapedParam = '""';
}
else
escapedParam = `"${sqlEscaper(params.content)}"`;
await connections[params.uid]
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${id}` })
.run();
return { status: 'success', response: { reload } };
}
catch (err) {
return { status: 'error', response: err.toString() };
@@ -46,8 +93,20 @@ 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 Tables.deleteTableRows(connections[params.uid], params);
const result = await connections[params.uid]
.schema(params.schema)
.delete(params.table)
.where({ [params.primary]: `IN (${idString})` })
.run();
return { status: 'success', response: result };
}
catch (err) {
@@ -57,7 +116,39 @@ export default (connections) => {
ipcMain.handle('insert-table-rows', async (event, params) => {
try {
await Tables.insertTableRows(connections[params.uid], params);
const insertObj = {};
for (const key in params.row) {
const type = params.fields[key];
let escapedParam;
if (params.row[key] === null)
escapedParam = 'NULL';
else if (NUMBER.includes(type))
escapedParam = params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type))
escapedParam = `"${sqlEscaper(params.row[key])}"`;
else if (BLOB.includes(type)) {
if (params.row[key]) {
const fileBlob = fs.readFileSync(params.row[key]);
escapedParam = `0x${fileBlob.toString('hex')}`;
}
else
escapedParam = '""';
}
else
escapedParam = `"${sqlEscaper(params.row[key])}"`;
insertObj[key] = escapedParam;
}
for (let i = 0; i < params.repeat; i++) {
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(insertObj)
.run();
}
return { status: 'success' };
}
catch (err) {
@@ -65,13 +156,63 @@ export default (connections) => {
}
});
ipcMain.handle('get-foreign-list', async (event, params) => {
ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => {
try {
const results = await Tables.getForeignList(connections[params.uid], params);
const query = connections[uid]
.select(`${column} AS foreignColumn`)
.schema(schema)
.from(table)
.orderBy('foreignColumn ASC');
if (description)
query.select(`LEFT(${description}, 20) AS foreignDescription`);
const results = await query.run();
return { status: 'success', response: results };
}
catch (err) {
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

@@ -1,328 +0,0 @@
'use strict';
import mysql from 'mysql';
import mssql from 'mssql';
// import pg from 'pg'; TODO: PostgreSQL
/**
* As Simple As Possible Query Builder
*
* @export
* @class AntaresConnector
*/
export class AntaresConnector {
/**
*Creates an instance of AntaresConnector.
* @param {Object} args connection params
* @memberof AntaresConnector
*/
constructor (args) {
this._client = args.client;
this._params = args.params;
this._poolSize = args.poolSize || false;
this._connection = null;
this._logger = args.logger || console.log;
this._queryDefaults = {
schema: '',
select: [],
from: '',
where: [],
groupBy: [],
orderBy: [],
limit: [],
join: [],
update: [],
insert: {},
delete: false
};
this._query = Object.assign({}, this._queryDefaults);
}
_reducer (acc, curr) {
const type = typeof curr;
switch (type) {
case 'number':
case 'string':
return [...acc, curr];
case 'object':
if (Array.isArray(curr))
return [...acc, ...curr];
else {
const clausoles = [];
for (const key in curr)
clausoles.push(`${key} ${curr[key]}`);
return clausoles;
}
}
}
/**
* Resets the query object after a query
*
* @memberof AntaresConnector
*/
_resetQuery () {
this._query = Object.assign({}, this._queryDefaults);
}
/**
* @memberof AntaresConnector
*/
async connect () {
switch (this._client) {
case 'maria':
case 'mysql':
if (!this._poolSize)
this._connection = mysql.createConnection(this._params);
else
this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize });
break;
case 'mssql': {
const mssqlParams = {
user: this._params.user,
password: this._params.password,
server: this._params.host
};
this._connection = await mssql.connect(mssqlParams);
}
break;
default:
break;
}
}
schema (schema) {
this._query.schema = schema;
return this;
}
select (...args) {
this._query.select = [...this._query.select, ...args];
return this;
}
from (table) {
this._query.from = table;
return this;
}
into (table) {
this._query.from = table;
return this;
}
delete (table) {
this._query.delete = true;
this.from(table);
return this;
}
where (...args) {
this._query.where = [...this._query.where, ...args];
return this;
}
groupBy (...args) {
this._query.groupBy = [...this._query.groupBy, ...args];
return this;
}
orderBy (...args) {
this._query.orderBy = [...this._query.orderBy, ...args];
return this;
}
limit (...args) {
this._query.limit = args;
return this;
}
use (schema) {
let sql;
switch (this._client) {
case 'maria':
case 'mysql':
sql = `USE \`${schema}\``;
break;
case 'mssql':
sql = `USE "${schema}"`;
break;
default:
break;
}
return this.raw(sql);
}
/**
* @param {String | Array} args field = value
* @returns
* @memberof AntaresConnector
*/
update (...args) {
this._query.update = [...this._query.update, ...args];
return this;
}
/**
* @param {Object} obj field: value
* @returns
* @memberof AntaresConnector
*/
insert (obj) {
this._query.insert = { ...this._query.insert, ...obj };
return this;
}
/**
* @returns {string} SQL string
* @memberof AntaresConnector
*/
getSQL () {
// SELECT
const selectArray = this._query.select.reduce(this._reducer, []);
let selectRaw = '';
if (selectArray.length) {
switch (this._client) {
case 'maria':
case 'mysql':
selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * ';
break;
case 'mssql': {
const topRaw = this._query.limit.length ? ` TOP (${this._query.limit[0]}) ` : '';
selectRaw = selectArray.length ? `SELECT${topRaw} ${selectArray.join(', ')} ` : 'SELECT * ';
}
break;
default:
break;
}
}
// FROM
let fromRaw = '';
if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from)
fromRaw = 'FROM';
else if (Object.keys(this._query.insert).length)
fromRaw = 'INTO';
switch (this._client) {
case 'maria':
case 'mysql':
fromRaw += this._query.from ? ` ${this._query.schema ? `\`${this._query.schema}\`.` : ''}\`${this._query.from}\` ` : '';
break;
case 'mssql':
fromRaw += this._query.from ? ` ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : '';
break;
default:
break;
}
const whereArray = this._query.where.reduce(this._reducer, []);
const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : '';
const updateArray = this._query.update.reduce(this._reducer, []);
const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : '';
let insertRaw = '';
if (Object.keys(this._query.insert).length) {
const fieldsList = [];
const valueList = [];
const fields = this._query.insert;
for (const key in fields) {
if (fields[key] === null) continue;
fieldsList.push(key);
valueList.push(fields[key]);
}
insertRaw = `(${fieldsList.join(', ')}) VALUES (${valueList.join(', ')}) `;
}
const groupByArray = this._query.groupBy.reduce(this._reducer, []);
const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : '';
const orderByArray = this._query.orderBy.reduce(this._reducer, []);
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
// LIMIT
let limitRaw;
switch (this._client) {
case 'maria':
case 'mysql':
limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
break;
case 'mssql':
limitRaw = '';
break;
default:
break;
}
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`;
}
/**
* @returns {Promise}
* @memberof AntaresConnector
*/
async run () {
const rawQuery = this.getSQL();
this._resetQuery();
return this.raw(rawQuery);
}
/**
* @param {string} sql raw SQL query
* @returns {Promise}
* @memberof AntaresConnector
*/
async raw (sql) {
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
switch (this._client) { // TODO: uniform fields with every client type, needed table name and fields array
case 'maria':
case 'mysql': {
const { rows, report, fields } = await new Promise((resolve, reject) => {
this._connection.query(sql, (err, response, fields) => {
if (err)
reject(err);
else {
resolve({
rows: Array.isArray(response) ? response : false,
report: !Array.isArray(response) ? response : false,
fields
});
}
});
});
return { rows, report, fields };
}
case 'mssql': {
const results = await this._connection.request().query(sql);
return { rows: results.recordsets[0] };// TODO: fields
}
default:
break;
}
}
/**
* @memberof AntaresConnector
*/
destroy () {
switch (this._client) {
case 'maria':
case 'mysql':
this._connection.end();
break;
case 'mssql':
this._connection.close();
break;
default:
break;
}
}
}

View File

@@ -0,0 +1,142 @@
'use strict';
/**
* As Simple As Possible Query Builder Core
*
* @class AntaresCore
*/
export class AntaresCore {
/**
* Creates an instance of AntaresCore.
*
* @param {Object} args connection params
* @memberof AntaresCore
*/
constructor (args) {
this._client = args.client;
this._params = args.params;
this._poolSize = args.poolSize || false;
this._connection = null;
this._logger = args.logger || console.log;
this._queryDefaults = {
schema: '',
select: [],
from: '',
where: [],
groupBy: [],
orderBy: [],
limit: [],
join: [],
update: [],
insert: {},
delete: false
};
this._query = Object.assign({}, this._queryDefaults);
}
_reducer (acc, curr) {
const type = typeof curr;
switch (type) {
case 'number':
case 'string':
return [...acc, curr];
case 'object':
if (Array.isArray(curr))
return [...acc, ...curr];
else {
const clausoles = [];
for (const key in curr)
clausoles.push(`${key} ${curr[key]}`);
return clausoles;
}
}
}
/**
* Resets the query object after a query
*
* @memberof AntaresCore
*/
_resetQuery () {
this._query = Object.assign({}, this._queryDefaults);
}
schema (schema) {
this._query.schema = schema;
return this;
}
select (...args) {
this._query.select = [...this._query.select, ...args];
return this;
}
from (table) {
this._query.from = table;
return this;
}
into (table) {
this._query.from = table;
return this;
}
delete (table) {
this._query.delete = true;
this.from(table);
return this;
}
where (...args) {
this._query.where = [...this._query.where, ...args];
return this;
}
groupBy (...args) {
this._query.groupBy = [...this._query.groupBy, ...args];
return this;
}
orderBy (...args) {
this._query.orderBy = [...this._query.orderBy, ...args];
return this;
}
limit (...args) {
this._query.limit = args;
return this;
}
/**
* @param {String | Array} args field = value
* @returns
* @memberof AntaresCore
*/
update (...args) {
this._query.update = [...this._query.update, ...args];
return this;
}
/**
* @param {Object} obj field: value
* @returns
* @memberof AntaresCore
*/
insert (obj) {
this._query.insert = { ...this._query.insert, ...obj };
return this;
}
/**
* @param {Object} args
* @returns {Promise}
* @memberof AntaresCore
*/
async run (args) {
const rawQuery = this.getSQL();
this._resetQuery();
return this.raw(rawQuery, args);
}
}

View File

@@ -0,0 +1,27 @@
'use strict';
import { MySQLClient } from './clients/MySQLClient';
export class ClientsFactory {
/**
* Returns a database connection based on received args.
*
* @param {Object} args
* @param {String} args.client
* @param {Object} args.params
* @param {String} args.params.host
* @param {Number} args.params.port
* @param {String} args.params.password
* @param {Number=} args.poolSize
* @returns Database Connection
* @memberof ClientsFactory
*/
static getConnection (args) {
switch (args.client) {
case 'mysql':
case 'maria':
return new MySQLClient(args);
default:
return new Error(`Unknown database client: ${args.client}`);
}
}
}

View File

@@ -0,0 +1,690 @@
'use strict';
import mysql from 'mysql';
import { AntaresCore } from '../AntaresCore';
export class MySQLClient extends AntaresCore {
/**
* @memberof MySQLClient
*/
async connect () {
if (!this._poolSize)
this._connection = mysql.createConnection(this._params);
else
this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize });
}
/**
* @memberof MySQLClient
*/
destroy () {
this._connection.end();
}
/**
* Executes an USE query
*
* @param {String} schema
* @memberof MySQLClient
*/
use (schema) {
return this.raw(`USE \`${schema}\``);
}
/**
* @returns {Array.<Object>} databases scructure
* @memberof MySQLClient
*/
async getStructure () {
const { rows: databases } = await this.raw('SHOW DATABASES');
// TODO: SHOW TABLE STATUS FROM `{DATABASE_NAME}`;
const { rows: tables } = await this
.select('*')
.schema('information_schema')
.from('TABLES')
.orderBy({ TABLE_SCHEMA: 'ASC', TABLE_NAME: 'ASC' })
.run();
const { rows: functions } = await this.raw('SHOW FUNCTION STATUS');
const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS');
const { rows: schedulers } = await this.raw('SELECT *, EVENT_SCHEMA AS `Db`, EVENT_NAME AS `Name` FROM information_schema.`EVENTS`');
const triggersArr = [];
for (const db of databases) {
let { rows: triggers } = await this.raw(`SHOW TRIGGERS FROM \`${db.Database}\``);
if (triggers.length) {
triggers = triggers.map(trigger => {
trigger.Db = db.Database;
return trigger;
});
triggersArr.push(...triggers);
}
}
return databases.map(db => {
// TABLES
const remappedTables = tables.filter(table => table.TABLE_SCHEMA === db.Database).map(table => {
let tableType;
switch (table.TABLE_TYPE) {
case 'VIEW':
tableType = 'view';
break;
default:
tableType = 'table';
break;
}
return {
name: table.TABLE_NAME,
type: tableType,
rows: table.TABLE_ROWS,
created: table.CREATE_TIME,
updated: table.UPDATE_TIME,
engine: table.ENGINE,
comment: table.TABLE_COMMENT,
size: table.DATA_LENGTH + table.INDEX_LENGTH,
autoIncrement: table.AUTO_INCREMENT,
collation: table.TABLE_COLLATION
};
});
// PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
return {
name: procedure.Name,
type: procedure.Type,
definer: procedure.Definer,
created: procedure.Created,
updated: procedure.Modified,
comment: procedure.Comment,
charset: procedure.character_set_client,
security: procedure.Security_type
};
});
// SCHEDULERS
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return {
name: scheduler.EVENT_NAME,
definition: scheduler.EVENT_DEFINITION,
type: scheduler.EVENT_TYPE,
definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS,
ends: scheduler.ENDS,
status: scheduler.STATUS,
executeAt: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD,
intervalValue: scheduler.INTERVAL_VALUE,
onCompletion: scheduler.ON_COMPLETION,
originator: scheduler.ORIGINATOR,
sqlMode: scheduler.SQL_MODE,
created: scheduler.CREATED,
updated: scheduler.LAST_ALTERED,
lastExecuted: scheduler.LAST_EXECUTED,
comment: scheduler.EVENT_COMMENT,
charset: scheduler.CHARACTER_SET_CLIENT,
timezone: scheduler.TIME_ZONE
};
});
// TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => {
return {
name: trigger.Trigger,
statement: trigger.Statement,
timing: trigger.Timing,
definer: trigger.Definer,
event: trigger.Event,
table: trigger.Table,
sqlMode: trigger.sql_mode,
created: trigger.Created,
charset: trigger.character_set_client
};
});
return {
name: db.Database,
tables: remappedTables,
functions: functions.filter(func => func.Db === db.Database), // TODO: remap functions
procedures: remappedProcedures,
triggers: remappedTriggers,
schedulers: remappedSchedulers
};
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table scructure
* @memberof MySQLClient
*/
async getTableColumns ({ schema, table }) {
const { rows } = await this
.select('*')
.schema('information_schema')
.from('COLUMNS')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` })
.orderBy({ ORDINAL_POSITION: 'ASC' })
.run();
return rows.map(field => {
let numLength = field.COLUMN_TYPE.match(/int\(([^)]+)\)/);
numLength = numLength ? +numLength.pop() : null;
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE.toUpperCase(),
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION,
numLength,
datePrecision: field.DATETIME_PRECISION,
charLength: field.CHARACTER_MAXIMUM_LENGTH,
nullable: field.IS_NULLABLE.includes('YES'),
unsigned: field.COLUMN_TYPE.includes('unsigned'),
zerofill: field.COLUMN_TYPE.includes('zerofill'),
order: field.ORDINAL_POSITION,
default: field.COLUMN_DEFAULT,
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment'),
onUpdate: field.EXTRA.toLowerCase().includes('on update') ? field.EXTRA.replace('on update', '') : '',
comment: field.COLUMN_COMMENT
};
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table indexes
* @memberof MySQLClient
*/
async getTableIndexes ({ schema, table }) {
const { rows } = await this.raw(`SHOW INDEXES FROM \`${table}\` FROM \`${schema}\``);
return rows.map(row => {
return {
unique: !row.Non_unique,
name: row.Key_name,
column: row.Column_name,
indexType: row.Index_type,
type: row.Key_name === 'PRIMARY' ? 'PRIMARY' : !row.Non_unique ? 'UNIQUE' : row.Index_type === 'FULLTEXT' ? 'FULLTEXT' : 'INDEX',
cardinality: row.Cardinality,
comment: row.Comment,
indexComment: row.Index_comment
};
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table key usage
* @memberof MySQLClient
*/
async getKeyUsage ({ schema, table }) {
const { rows } = await this
.select('*')
.schema('information_schema')
.from('KEY_COLUMN_USAGE')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run();
const { rows: extras } = await this
.select('*')
.schema('information_schema')
.from('REFERENTIAL_CONSTRAINTS')
.where({ CONSTRAINT_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run();
return rows.map(field => {
const extra = extras.find(x => x.CONSTRAINT_NAME === field.CONSTRAINT_NAME);
return {
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
field: field.COLUMN_NAME,
position: field.ORDINAL_POSITION,
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
constraintName: field.CONSTRAINT_NAME,
refSchema: field.REFERENCED_TABLE_SCHEMA,
refTable: field.REFERENCED_TABLE_NAME,
refField: field.REFERENCED_COLUMN_NAME,
onUpdate: extra.UPDATE_RULE,
onDelete: extra.DELETE_RULE
};
});
}
/**
* SHOW COLLATION
*
* @returns {Array.<Object>} collations list
* @memberof MySQLClient
*/
async getCollations () {
const results = await this.raw('SHOW COLLATION');
return results.rows.map(row => {
return {
charset: row.Charset,
collation: row.Collation,
compiled: row.Compiled.includes('Yes'),
default: row.Default.includes('Yes'),
id: row.Id,
sortLen: row.Sortlen
};
});
}
/**
* SHOW VARIABLES
*
* @returns {Array.<Object>} variables list
* @memberof MySQLClient
*/
async getVariables () {
const sql = 'SHOW VARIABLES';
const results = await this.raw(sql);
return results.rows.map(row => {
return {
name: row.Variable_name,
value: row.Value
};
});
}
/**
* 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
*/
getSQL () {
// SELECT
const selectArray = this._query.select.reduce(this._reducer, []);
let selectRaw = '';
if (selectArray.length)
selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * ';
// FROM
let fromRaw = '';
if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from)
fromRaw = 'FROM';
else if (Object.keys(this._query.insert).length)
fromRaw = 'INTO';
fromRaw += this._query.from ? ` ${this._query.schema ? `\`${this._query.schema}\`.` : ''}\`${this._query.from}\` ` : '';
// WHERE
const whereArray = this._query.where.reduce(this._reducer, []);
const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : '';
// UPDATE
const updateArray = this._query.update.reduce(this._reducer, []);
const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : '';
// INSERT
let insertRaw = '';
if (Object.keys(this._query.insert).length) {
const fieldsList = [];
const valueList = [];
const fields = this._query.insert;
for (const key in fields) {
if (fields[key] === null) continue;
fieldsList.push(key);
valueList.push(fields[key]);
}
insertRaw = `(${fieldsList.join(', ')}) VALUES (${valueList.join(', ')}) `;
}
// GROUP BY
const groupByArray = this._query.groupBy.reduce(this._reducer, []);
const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : '';
// ORDER BY
const orderByArray = this._query.orderBy.reduce(this._reducer, []);
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
// LIMIT
const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`;
}
/**
* @param {string} sql raw SQL query
* @param {object} args
* @param {boolean} args.nest
* @param {boolean} args.details
* @returns {Promise}
* @memberof MySQLClient
*/
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;
if (err)
reject(err);
else {
const remappedFields = fields
? fields.map(field => {
return {
name: field.name,
orgName: field.orgName,
schema: field.db,
table: field.table,
orgTable: field.orgTable,
type: 'varchar'
};
})
: [];
if (args.details) {
let cachedTable;
if (remappedFields.length) {
selectedFields = remappedFields.map(field => {
return {
name: field.orgName || field.name,
table: field.orgTable || field.table
};
});
paramsArr = remappedFields.map(field => {
if (field.table) cachedTable = field.table;// Needed for some queries on information_schema
return {
table: field.orgTable || cachedTable,
schema: field.schema || 'INFORMATION_SCHEMA'
};
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
for (const paramObj of paramsArr) {
try { // Table data
const response = await this.getTableColumns(paramObj);
let detailedFields = response.length
? selectedFields.map(selField => {
return response.find(field => field.name === selField.name && field.table === selField.table);
}).filter(el => !!el)
: [];
if (selectedFields.length) {
detailedFields = detailedFields.map(field => {
const aliasObj = remappedFields.find(resField => resField.orgName === field.name);
return {
...field,
alias: aliasObj.name || field.name,
tableAlias: aliasObj.table || field.table
};
});
}
if (!detailedFields.length) {
detailedFields = remappedFields.map(field => {
return {
...field,
alias: field.name,
tableAlias: field.table
};
});
}
fieldsArr = fieldsArr ? [...fieldsArr, ...detailedFields] : detailedFields;
}
catch (err) {
reject(err);
}
try { // Key usage (foreign keys)
const response = await this.getKeyUsage(paramObj);
keysArr = keysArr ? [...keysArr, ...response] : response;
}
catch (err) {
reject(err);
}
}
}
}
resolve({
rows: Array.isArray(queryResult) ? queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false,
fields: fieldsArr.length ? fieldsArr : remappedFields,
keys: keysArr
});
}
});
});
resultsArr.push({ rows, report, fields, keys });
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
}
}

View File

@@ -1,14 +0,0 @@
'use strict';
export default class {
static async raw (connection, query, schema) {
if (schema) {
try {
await connection.use(schema);
}
catch (err) {
return err;
}
}
return connection.raw(query);
}
}

View File

@@ -1,64 +0,0 @@
'use strict';
export default class {
static testConnection (connection) {
return connection.select('1+1').run();
}
static getStructure (connection) {
return connection
.select('*')
.schema('information_schema')
.from('TABLES')
.orderBy({ TABLE_SCHEMA: 'ASC', TABLE_NAME: 'ASC' })
.run();
}
static async getTableColumns (connection, schema, table) {
const { rows } = await connection
.select('*')
.schema('information_schema')
.from('COLUMNS')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` })
.orderBy({ ORDINAL_POSITION: 'ASC' })
.run();
return rows.map(field => {
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE,
numPrecision: field.NUMERIC_PRECISION,
datePrecision: field.DATETIME_PRECISION,
charLength: field.CHARACTER_MAXIMUM_LENGTH,
isNullable: field.IS_NULLABLE,
default: field.COLUMN_DEFAULT,
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment')
};
});
}
static async getKeyUsage (connection, schema, table) {
const { rows } = await connection
.select('*')
.schema('information_schema')
.from('KEY_COLUMN_USAGE')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run();
return rows.map(field => {
return {
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
column: field.COLUMN_NAME,
position: field.ORDINAL_POSITION,
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
constraintName: field.CONSTRAINT_NAME,
refSchema: field.REFERENCED_TABLE_SCHEMA,
refTable: field.REFERENCED_TABLE_NAME,
refColumn: field.REFERENCED_COLUMN_NAME
};
});
}
}

View File

@@ -1,102 +0,0 @@
'use strict';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes';
import fs from 'fs';
export default class {
static async getTableData (connection, schema, table) {
return connection
.select('*')
.schema(schema)
.from(table)
.limit(1000)
.run();
}
static async updateTableCell (connection, params) {
let escapedParam;
let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
if (NUMBER.includes(params.type))
escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type))
escapedParam = `"${sqlEscaper(params.content)}"`;
else if (BLOB.includes(params.type)) {
if (params.content) {
const fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
reload = true;
}
else
escapedParam = '""';
}
else
escapedParam = `"${sqlEscaper(params.content)}"`;
await connection
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${id}` })
.run();
return { reload };
}
static async deleteTableRows (connection, params) {
return connection
.schema(params.schema)
.delete(params.table)
.where({ [params.primary]: `IN (${params.rows.join(',')})` })
.run();
}
static async insertTableRows (connection, params) {
const insertObj = {};
for (const key in params.row) {
const type = params.fields[key];
let escapedParam;
if (params.row[key] === null)
escapedParam = 'NULL';
else if (NUMBER.includes(type))
escapedParam = params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type))
escapedParam = `"${sqlEscaper(params.row[key])}"`;
else if (BLOB.includes(type)) {
if (params.row[key]) {
const fileBlob = fs.readFileSync(params.row[key]);
escapedParam = `0x${fileBlob.toString('hex')}`;
}
else
escapedParam = '""';
}
else
escapedParam = `"${sqlEscaper(params.row[key])}"`;
insertObj[key] = escapedParam;
}
for (let i = 0; i < params.repeat; i++) {
await connection
.schema(params.schema)
.into(params.table)
.insert(insertObj)
.run();
}
}
static async getForeignList (connection, params) {
const query = connection
.select(`${params.column} AS foreignColumn`)
.schema(params.schema)
.from(params.table)
.orderBy('foreignColumn ASC');
if (params.description)
query.select(`LEFT(${params.description}, 20) AS foreignDescription`);
return query.run();
}
}

View File

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

View File

@@ -27,7 +27,7 @@
<div class="modal-footer">
<button
class="btn btn-primary mr-2"
@click="confirmModal"
@click.stop="confirmModal"
>
{{ confirmText || $t('word.confirm') }}
</button>
@@ -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

@@ -1,7 +1,12 @@
<template>
<div class="context">
<a
class="context-overlay"
@click="close"
@contextmenu="close"
/>
<div
v-click-outside="close"
ref="contextContent"
class="context-container"
:style="position"
>
@@ -11,27 +16,53 @@
</template>
<script>
import ClickOutside from 'vue-click-outside';
export default {
name: 'BaseContextMenu',
directives: {
ClickOutside
},
props: {
contextEvent: MouseEvent
},
data () {
return {
contextSize: null
};
},
computed: {
position () {
return { // TODO: calc direction if near corners
top: this.contextEvent.clientY + 5 + 'px',
left: this.contextEvent.clientX + 5 + 'px'
const { clientY, clientX } = this.contextEvent;
let topCord = `${clientY + 5}px`;
let leftCord = `${clientX + 5}px`;
if (this.contextSize) {
if (clientY + this.contextSize.height + 5 >= window.innerHeight)
topCord = `${clientY - this.contextSize.height}px`;
if (clientX + this.contextSize.width + 5 >= window.innerWidth)
leftCord = `${clientX - this.contextSize.width}px`;
}
return {
top: topCord,
left: leftCord
};
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
close () {
this.$emit('close-context');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.close();
}
}
};
@@ -52,13 +83,11 @@ export default {
top: 0;
left: 0;
bottom: 0;
pointer-events: none;
.context-container {
min-width: 100px;
max-width: 150px;
z-index: 10;
box-shadow: 0 0 1px 0 #000;
box-shadow: 0 0 2px 0 #000;
padding: 0;
background: #1d1d1d;
border-radius: 0.1rem;
@@ -72,9 +101,29 @@ export default {
align-items: center;
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

@@ -10,7 +10,7 @@
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body">
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<div class="form-group">
@@ -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

@@ -10,7 +10,7 @@
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
<div class="modal-body">
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
@@ -20,6 +20,7 @@
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="localConnection.name"
class="form-input"
type="text"
@@ -113,13 +114,13 @@
</fieldset>
</form>
</div>
</div>
<div class="modal-footer text-light">
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div class="modal-footer text-light">
<button
class="btn btn-gray mr-2"
:class="{'loading': isTesting}"
@@ -144,7 +145,7 @@
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapActions } from 'vuex';
import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast';
@@ -155,6 +156,9 @@ export default {
ModalAskCredentials,
BaseToast
},
props: {
connection: Object
},
data () {
return {
toast: {
@@ -166,17 +170,19 @@ export default {
localConnection: null
};
},
computed: {
...mapGetters({
connection: 'application/getSelectedConnection'
})
},
created () {
this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
closeModal: 'application/hideEditConnModal',
editConnection: 'connections/editConnection'
}),
async startTest () {
@@ -226,6 +232,14 @@ export default {
closeAsking () {
this.isTesting = false;
this.isAsking = false;
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};

View File

@@ -0,0 +1,169 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editDatabase') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}:</label>
</div>
<div class="col-9">
<input
v-model="database.name"
class="form-input"
type="text"
required
:placeholder="$t('message.databaseName')"
readonly
>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}:</label>
</div>
<div class="col-9">
<select
ref="firstInput"
v-model="database.collation"
class="form-select"
>
<option
v-for="collation in collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
</div>
</div>
<div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="updateDatabase">
{{ $t('word.update') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database';
export default {
name: 'ModalEditDatabase',
props: {
selectedDatabase: String
},
data () {
return {
database: {
name: '',
prevName: '',
collation: ''
}
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
},
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
}
},
async created () {
let actualCollation;
try {
const { status, response } = await Database.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase });
if (status === 'success')
actualCollation = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.database = {
name: this.selectedDatabase,
prevName: this.selectedDatabase,
collation: actualCollation || this.defaultCollation,
prevCollation: actualCollation || this.defaultCollation
};
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async updateDatabase () {
if (this.database.collation !== this.database.prevCollation) {
try {
const { status, response } = await Database.updateDatabase({
uid: this.selectedWorkspace,
...this.database
});
if (status === 'success')
this.closeModal();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
else
this.closeModal();
},
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

@@ -10,7 +10,7 @@
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
<div class="modal-body">
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isTesting">
@@ -20,6 +20,7 @@
</div>
<div class="col-8 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
@@ -117,13 +118,13 @@
</fieldset>
</form>
</div>
</div>
<div class="modal-footer text-light">
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div class="modal-footer text-light">
<button
class="btn btn-gray mr-2"
:class="{'loading': isTesting}"
@@ -180,6 +181,16 @@ export default {
isAsking: false
};
},
created () {
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
closeModal: 'application/hideNewConnModal',
@@ -249,6 +260,11 @@ export default {
closeAsking () {
this.isAsking = false;
this.isTesting = false;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};

View File

@@ -0,0 +1,138 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewDatabase') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}:</label>
</div>
<div class="col-9">
<input
ref="firstInput"
v-model="database.name"
class="form-input"
type="text"
required
:placeholder="$t('message.databaseName')"
>
</div>
</div>
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}:</label>
</div>
<div class="col-9">
<select v-model="database.collation" class="form-select">
<option
v-for="collation in collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
</div>
</div>
</form>
</div>
</div>
<div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="createDatabase">
{{ $t('word.add') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database';
export default {
name: 'ModalNewDatabase',
data () {
return {
database: {
name: '',
collation: ''
}
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
},
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
}
},
created () {
this.database = { ...this.database, collation: this.defaultCollation };
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async createDatabase () {
try {
const { status, response } = await Database.createDatabase({
uid: this.selectedWorkspace,
...this.database
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
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

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

@@ -10,7 +10,7 @@
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body">
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset :disabled="isInserting">
@@ -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 : [];
},
keyUsage () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage : [];
return this.keyUsage.map(key => key.field);
}
},
watch: {
@@ -160,6 +159,9 @@ export default {
this.nInserts = 1;
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
const rowObj = {};
@@ -194,6 +196,15 @@ export default {
}
this.localRow = { ...rowObj };
// Auto focus
setTimeout(() => {
const firstSelectableInput = this.$refs.formInput.find(input => !input.disabled);
firstSelectableInput.focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
@@ -242,7 +253,7 @@ export default {
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numPrecision || field.datePrecision || field.charLength || 0;
return field.numLength || field.datePrecision || field.charLength || 0;
},
inputProps (field) {
if ([...TEXT, ...LONG_TEXT].includes(field.type))
@@ -294,9 +305,13 @@ export default {
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName);
return this.keyUsage.find(key => key.field === keyName);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};

View File

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

View File

@@ -16,6 +16,9 @@
{{ downloadPercentage }}%
</p>
</div>
<div v-if="updateStatus === 'available'">
<progress class="progress" max="100" />
</div>
<div class="empty-action">
<button
v-if="['noupdate', 'checking', 'nocheck'].includes(updateStatus)"

View File

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

View File

@@ -4,15 +4,20 @@
@close-context="$emit('close-context')"
>
<div class="context-element" @click="showEditModal(contextConnection)">
<i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}
<span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span>
</div>
<div class="context-element" @click="showConfirmModal">
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}</span>
</div>
<ModalEditConnection
v-if="isEditModal"
:connection="contextConnection"
@close="hideEditModal"
/>
<ConfirmModal
v-if="isConfirmModal"
@confirm="deleteConnection(contextConnection)"
@confirm="confirmDeleteConnection"
@hide="hideConfirmModal"
>
<template :slot="'header'">
@@ -22,7 +27,7 @@
</template>
<div :slot="'body'">
<div class="mb-2">
{{ $t('message.deleteConnectionCorfirm') }} <b>{{ connectionName }}</b>?
{{ $t('message.deleteCorfirm') }} <b>{{ connectionName }}</b>?
</div>
</div>
</ConfirmModal>
@@ -33,11 +38,13 @@
import { mapActions, mapGetters } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditConnection from '@/components/ModalEditConnection';
export default {
name: 'SettingBarContext',
components: {
BaseContextMenu,
ModalEditConnection,
ConfirmModal
},
props: {
@@ -46,7 +53,8 @@ export default {
},
data () {
return {
isConfirmModal: false
isConfirmModal: false,
isEditModal: false
};
},
computed: {
@@ -59,14 +67,28 @@ export default {
},
methods: {
...mapActions({
deleteConnection: 'connections/deleteConnection',
showEditModal: 'application/showEditConnModal'
deleteConnection: 'connections/deleteConnection'
}),
confirmDeleteConnection () {
this.deleteConnection(this.contextConnection);
this.closeContext();
},
showEditModal () {
this.isEditModal = true;
},
hideEditModal () {
this.isEditModal = false;
this.closeContext();
},
showConfirmModal () {
this.isConfirmModal = true;
},
hideConfirmModal () {
this.isConfirmModal = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
}
}
};

View File

@@ -2,7 +2,7 @@
<div id="footer" class="text-light">
<div class="footer-left-elements">
<ul class="footer-elements">
<li class="footer-element">
<li class="footer-element" :title="$t('word.version')">
<i class="mdi mdi-18px mdi-memory mr-1" />
<small>{{ appVersion }}</small>
</li>
@@ -15,7 +15,7 @@
<i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/EStarium/antares/issues')">
<li class="footer-element footer-link" @click="openOutside('https://github.com/Fabio286/antares/issues')">
<i class="mdi mdi-18px mdi-bug" />
</li>
<li class="footer-element footer-link" @click="showSettingModal('about')">

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,9 +40,10 @@
class="tab-item"
:class="{'active': selectedTab === tab.uid}"
@click="selectTab({uid: workspace.uid, tab: tab.uid})"
@mousedown.middle="closeTab(tab.uid)"
@mouseup.middle="closeTab(tab.uid)"
>
<a>
<a class="tab-link">
<i class="mdi mdi-18px mdi-code-tags mr-1" />
<span>
Query #{{ tab.index }}
<span
@@ -55,6 +65,12 @@
</a>
</li>
</ul>
<WorkspacePropsTab
v-show="selectedTab === 'prop'"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:table="workspace.breadcrumbs.table"
/>
<WorkspaceTableTab
v-show="selectedTab === 'data'"
:connection="connection"
@@ -77,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',
@@ -100,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');
@@ -131,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;
@@ -156,6 +193,7 @@ export default {
align-items: flex-start;
flex-wrap: nowrap;
overflow: auto;
margin-bottom: 0;
&::-webkit-scrollbar {
width: 2px;
@@ -197,6 +235,11 @@ export default {
&.active a {
opacity: 1;
}
.workspace-tools-link {
padding-bottom: 0;
padding-top: 0.3rem;
}
}
}
}

View File

@@ -5,7 +5,7 @@
<i class="mdi mdi-48px mdi-power-plug-off" />
</div>
<p class="empty-title h5">
{{ $t('word.disconnected') }}
{{ isConnecting ? $t('word.connecting') : $t('word.disconnected') }}
</p>
<div class="empty-action">
<button

View File

@@ -10,13 +10,18 @@
<span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connected" class="workspace-explorebar-tools">
<i
class="mdi mdi-18px mdi-refresh c-hand"
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewDatabase')"
@click="showNewDBModal"
/>
<i
class="mdi mdi-18px mdi-refresh c-hand mr-2"
:class="{'rotate':isRefreshing}"
:title="$t('word.refresh')"
@click="refresh"
/>
<i
class="mdi mdi-18px mdi-power-plug-off c-hand mr-1 ml-2"
class="mdi mdi-18px mdi-power-plug-off c-hand"
:title="$t('word.disconnect')"
@click="disconnectWorkspace(connection.uid)"
/>
@@ -33,23 +38,60 @@
:key="db.name"
:database="db"
:connection="connection"
@show-database-context="openDatabaseContext"
@show-table-context="openTableContext"
/>
</div>
</div>
<ModalNewDatabase
v-if="isNewDBModal"
@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>
</template>
<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',
components: {
WorkspaceConnectPanel,
WorkspaceExploreBarDatabase
WorkspaceExploreBarDatabase,
DatabaseContext,
TableContext,
ModalNewDatabase,
ModalNewTable
},
props: {
connection: Object,
@@ -58,7 +100,15 @@ export default {
data () {
return {
isRefreshing: false,
localWidth: null
isNewDBModal: false,
isNewTableModal: false,
localWidth: null,
isDatabaseContext: false,
isTableContext: false,
databaseContextEvent: null,
tableContextEvent: null,
selectedDatabase: '',
selectedTable: ''
};
},
computed: {
@@ -99,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 () {
@@ -117,6 +170,51 @@ export default {
},
stopResize () {
window.removeEventListener('mousemove', this.resize);
},
showNewDBModal () {
this.isNewDBModal = true;
},
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.selectedDatabase = payload.database;
this.databaseContextEvent = payload.event;
this.isDatabaseContext = true;
},
closeDatabaseContext () {
this.isDatabaseContext = false;
},
openTableContext (payload) {
this.selectedTable = payload.table;
this.tableContextEvent = payload.event;
this.isTableContext = true;
},
closeTableContext () {
this.isTableContext = false;
}
}
};

View File

@@ -1,9 +1,10 @@
<template>
<details class="accordion workspace-explorebar-database">
<summary
class="accordion-header database-name pb-0"
class="accordion-header database-name"
:class="{'text-bold': breadcrumbs.schema === database.name}"
@click="changeBreadcrumbs({schema: database.name, table:null})"
@click="changeBreadcrumbs({schema: database.name, table: null})"
@contextmenu.prevent="showDatabaseContext($event, database.name)"
>
<i class="icon mdi mdi-18px mdi-chevron-right" />
<i class="database-icon mdi mdi-18px mdi-database mr-1" />
@@ -14,15 +15,19 @@
<ul class="menu menu-nav pt-0">
<li
v-for="table of database.tables"
:key="table.TABLE_NAME"
:key="table.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.TABLE_NAME}"
@click="changeBreadcrumbs({schema: database.name, table: table.TABLE_NAME})"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.name}"
@click="setBreadcrumbs({schema: database.name, table: table.name})"
@contextmenu.prevent="showTableContext($event, table.name)"
>
<a class="table-name">
<i class="table-icon mdi mdi-18px mdi-table mr-1" />
<span>{{ table.TABLE_NAME }}</span>
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span>{{ table.name }}</span>
</a>
<div class="table-size tooltip tooltip-left mr-1" :data-tooltip="formatBytes(table.size)">
<div class="pie" :style="piePercentage(table.size)" />
</div>
</li>
</ul>
</div>
@@ -32,6 +37,7 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { formatBytes } from 'common/libs/formatBytes';
export default {
name: 'WorkspaceExploreBarDatabase',
@@ -45,51 +51,121 @@ export default {
}),
breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs;
},
maxSize () {
return this.database.tables.reduce((acc, curr) => {
if (curr.size > acc) acc = curr.size;
return acc;
}, 0);
},
totalSize () {
return this.database.tables.reduce((acc, curr) => acc + curr.size, 0);
}
},
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
})
}),
formatBytes,
showDatabaseContext (event, database) {
this.$emit('show-database-context', { event, database });
},
showTableContext (event, table) {
this.$emit('show-table-context', { event, table });
},
piePercentage (val) {
const perc = val / this.maxSize * 100;
return { background: `conic-gradient(lime ${perc}%, white 0)` };
},
setBreadcrumbs (payload) {
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
this.changeBreadcrumbs(payload);
}
}
};
</script>
<style lang="scss">
.workspace-explorebar-database {
.database-name,
a.table-name {
display: flex;
align-items: center;
padding: 0.1rem;
cursor: pointer;
font-size: 0.7rem;
.workspace-explorebar-database {
.database-name,
a.table-name {
display: flex;
align-items: center;
padding: 0.1rem 1rem 0.1rem 0.1rem;
cursor: pointer;
font-size: 0.7rem;
> span {
overflow: hidden;
white-space: nowrap;
display: block;
text-overflow: ellipsis;
}
&:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px;
}
.database-icon,
.table-icon {
opacity: 0.7;
}
> span {
overflow: hidden;
white-space: nowrap;
display: block;
text-overflow: ellipsis;
}
.menu-item {
line-height: 1.2;
}
.database-tables {
margin-left: 1.2rem;
.database-icon,
.table-icon {
opacity: 0.7;
}
}
.database-name {
&:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px;
}
}
a.table-name {
&:hover {
color: inherit;
background: inherit;
}
}
.menu-item {
line-height: 1.2;
position: relative;
&:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px;
}
}
.database-tables {
margin-left: 1.2rem;
}
.table-size {
position: absolute;
right: 0;
top: 0;
cursor: pointer;
display: flex;
align-items: center;
height: 100%;
opacity: 0.2;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
&::after {
font-weight: 400;
font-size: 0.5rem;
}
.pie {
width: 14px;
height: 14px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
}
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<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 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-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-database-remove text-light pr-1" /> {{ $t('word.delete') }}</span>
</div>
<ConfirmModal
v-if="isDeleteModal"
@confirm="deleteDatabase"
@hide="hideDeleteModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteDatabase') }}
</div>
</template>
<div slot="body">
<div class="mb-2">
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedDatabase }}</b>"?
</div>
</div>
</ConfirmModal>
<ModalEditDatabase
v-if="isEditModal"
:selected-database="selectedDatabase"
@close="hideEditModal"
/>
</BaseContextMenu>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditDatabase from '@/components/ModalEditDatabase';
import Database from '@/ipc-api/Database';
export default {
name: 'WorkspaceExploreBarDatabaseContext',
components: {
BaseContextMenu,
ConfirmModal,
ModalEditDatabase
},
props: {
contextEvent: MouseEvent,
selectedDatabase: String
},
data () {
return {
isDeleteModal: false,
isEditModal: 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;
},
showEditModal () {
this.isEditModal = true;
},
hideEditModal () {
this.isEditModal = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
},
async deleteDatabase () {
try {
const { status, response } = await Database.deleteDatabase({
uid: this.selectedWorkspace,
database: this.selectedDatabase
});
if (status === 'success') {
if (this.selectedDatabase === this.workspace.breadcrumbs.schema)
this.changeBreadcrumbs({ schema: 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,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,28 +1,39 @@
<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"
:workspace="workspace"
:schema="schema"
/>
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-link btn-sm"
class="btn btn-primary btn-sm"
:class="{'loading':isQuering}"
:disabled="!query"
title="F9"
@click="runQuery(query)"
>
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play text-success" />
<i class="mdi mdi-24px mdi-play" />
</button>
</div>
<div class="workspace-query-info">
<div v-if="results.rows">
{{ $t('word.results') }}: <b>{{ results.rows.length }}</b>
<div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
</div>
<div v-if="results.report">
{{ $t('message.affectedRows') }}: <b>{{ results.report.affectedRows }}</b>
<div v-if="affectedCount">
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b>
</div>
<div v-if="workspace.breadcrumbs.schema">
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.schema }}</b>
<div
v-if="workspace.breadcrumbs.schema"
class="d-flex"
:title="$t('word.schema')"
>
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ workspace.breadcrumbs.schema }}</b>
</div>
</div>
</div>
@@ -34,6 +45,8 @@
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:conn-uid="connection.uid"
mode="query"
@update-field="updateField"
@delete-selected="deleteSelected"
/>
@@ -42,8 +55,7 @@
</template>
<script>
import Connection from '@/ipc-api/Connection';
import Tables from '@/ipc-api/Tables';
import Database from '@/ipc-api/Database';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import { mapGetters, mapActions } from 'vuex';
@@ -66,8 +78,9 @@ export default {
query: '',
lastQuery: '',
isQuering: false,
results: {},
selectedFields: []
results: [],
resultsCount: 0,
affectedCount: 0
};
},
computed: {
@@ -76,28 +89,23 @@ export default {
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
table () {
if ('fields' in this.results && this.results.fields.length)
return this.results.fields[0].orgTable;
return '';
},
schema () {
if ('fields' in this.results && this.results.fields.length)
return this.results.fields[0].db;
return this.workspace.breadcrumbs.schema;
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
setTabFields: 'workspaces/setTabFields',
setTabKeyUsage: 'workspaces/setTabKeyUsage'
addNotification: 'notifications/addNotification'
}),
async runQuery (query) {
if (!query) return;
if (!query || this.isQuering) return;
this.isQuering = true;
this.clearTabData();
this.$refs.queryTable.resetSort();
try { // Query Data
const params = {
@@ -106,57 +114,12 @@ export default {
query
};
const { status, response } = await Connection.rawQuery(params);
const { status, response } = await Database.rawQuery(params);
if (status === 'success') {
this.results = response;
if (response.rows) { // if is a select
this.selectedFields = response.fields.map(field => field.orgName);
try { // Table data
const params = {
uid: this.connection.uid,
schema: this.schema,
table: this.table
};
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
let fields = response.filter(field => this.selectedFields.includes(field.name));
if (this.selectedFields.length) {
fields = fields.map((field, index) => {
return { ...field, alias: this.results.fields[index].name };
});
}
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, 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,
schema: this.schema,
table: this.table
};
const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success')
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: response });
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
else { // if is a query without results
}
this.results = Array.isArray(response) ? response : [response];
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 });
@@ -172,8 +135,16 @@ export default {
this.runQuery(this.lastQuery);
},
clearTabData () {
this.results = {};
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
this.results = [];
this.resultsCount = 0;
this.affectedCount = 0;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.key === 'F9')
this.runQuery(this.query);
}
}
}
};

View File

@@ -11,13 +11,25 @@
@delete-selected="deleteSelected"
@close-context="isContext = false"
/>
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
<li
v-for="(result, index) in resultsWithRows"
:key="index"
class="tab-item"
:class="{'active': resultsetIndex === index}"
@click="selectResultset(index)"
>
<a>{{ result.fields ? result.fields[0].table : '' }} ({{ result.rows.length }})</a>
</li>
</ul>
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr">
<div
v-for="field in fields"
:key="field.name"
v-for="(field, index) in fields"
:key="index"
class="th c-hand"
:title="`${field.type} ${fieldLength(field) ? `(${fieldLength(field)})` : ''}`"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field.name)">
@@ -29,7 +41,7 @@
/>
<span>{{ field.alias || field.name }}</span>
<i
v-if="currentSort === field.name"
v-if="currentSort === field.name || currentSort === `${field.table}.${field.name}`"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
@@ -39,7 +51,7 @@
</div>
</div>
<BaseVirtualScroll
v-if="results.rows"
v-if="resultsWithRows[resultsetIndex] && resultsWithRows[resultsetIndex].rows"
ref="resultTable"
:items="sortedResults"
:item-height="22"
@@ -57,17 +69,18 @@
class="tr"
:class="{'selected': selectedRows.includes(row._id)}"
@select-row="selectRow($event, row._id)"
@update-field="updateField($event, row[primaryField.alias || primaryField.name])"
@update-field="updateField($event, getPrimaryValue(row))"
@contextmenu="contextMenu"
/>
</template>
</basevirtualscroll>
</BaseVirtualScroll>
</div>
</div>
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import { LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext';
@@ -81,8 +94,10 @@ export default {
TableContext
},
props: {
results: Object,
tabUid: [String, Number]
results: Array,
tabUid: [String, Number],
connUid: String,
mode: String
},
data () {
return {
@@ -93,18 +108,26 @@ export default {
selectedCell: null,
selectedRows: [],
currentSort: '',
currentSortDir: 'asc'
currentSortDir: 'asc',
resultsetIndex: 0,
scrollElement: null
};
},
computed: {
...mapGetters({
getWorkspaceTab: 'workspaces/getWorkspaceTab'
getWorkspace: 'workspaces/getWorkspace'
}),
workspaceSchema () {
return this.getWorkspace(this.connUid).breadcrumbs.schema;
},
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];
@@ -118,27 +141,31 @@ export default {
else
return this.localResults;
},
resultsWithRows () {
return this.results.filter(result => result.rows);
},
fields () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields : [];
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].fields : [];
},
keyUsage () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage : [];
},
scrollElement () {
return this.$refs.tableWrapper;
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : [];
}
},
watch: {
results () {
this.resetSort();
this.localResults = this.results.rows ? this.results.rows.map(item => {
return { ...item, _id: uidGen() };
}) : [];
this.setLocalResults();
this.resultsetIndex = 0;
},
resultsetIndex () {
this.setLocalResults();
}
},
updated () {
if (this.$refs.table)
this.refreshScroller();
if (this.$refs.tableWrapper)
this.scrollElement = this.$refs.tableWrapper;
},
mounted () {
window.addEventListener('resize', this.resizeResults);
@@ -166,6 +193,10 @@ export default {
return length;
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
return field.numLength || field.datePrecision || field.charLength || 0;
},
keyName (key) {
switch (key) {
case 'pri':
@@ -178,6 +209,34 @@ export default {
return 'UNKNOWN ' + key;
}
},
getTable (index) {
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length)
return this.resultsWithRows[index].fields[0].table;
return '';
},
getSchema (index) {
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length)
return this.resultsWithRows[index].fields[0].schema;
return this.workspaceSchema;
},
getPrimaryValue (row) {
const primaryFieldName = Object.keys(row).find(prop => [
this.primaryField.alias,
this.primaryField.name,
`${this.primaryField.table}.${this.primaryField.alias}`,
`${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.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) {
const el = this.$refs.tableWrapper;
@@ -199,6 +258,8 @@ export default {
else {
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id,
...payload
};
@@ -209,19 +270,28 @@ 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),
table: this.getTable(this.resultsetIndex),
rows: rowIDs
};
this.$emit('delete-selected', params);
}
},
applyUpdate (params) {
const { primary, id, field, content } = params;
const { primary, id, field, table, content } = params;
this.localResults = this.localResults.map(row => {
if (row[primary] === id)
if (row[primary] === id)// only fieldName
row[field] = content;
else if (row[`${table}.${primary}`] === id)// table.fieldName
row[`${table}.${field}`] = content;
return row;
});
@@ -261,6 +331,9 @@ export default {
this.isContext = true;
},
sort (field) {
if (this.mode === 'query')
field = `${this.getTable(this.resultsetIndex)}.${field}`;
if (field === this.currentSort) {
if (this.currentSortDir === 'asc')
this.currentSortDir = 'desc';
@@ -271,16 +344,22 @@ export default {
this.currentSortDir = 'asc';
this.currentSort = field;
}
if (this.isHardSort)
this.$emit('hard-sort', { field: this.currentSort, dir: this.currentSortDir });
},
resetSort () {
this.currentSort = '';
this.currentSortDir = 'asc';
},
selectResultset (index) {
this.resultsetIndex = index;
}
}
};
</script>
<style lang="scss">
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
@@ -305,4 +384,9 @@ export default {
line-height: 1;
margin-left: 0.2rem;
}
.result-tabs {
background: transparent !important;
margin: 0;
}
</style>

View File

@@ -4,7 +4,7 @@
@close-context="closeContext"
>
<div class="context-element" @click="showConfirmModal">
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}</span>
</div>
<ConfirmModal
@@ -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);
}
@@ -186,7 +190,10 @@ export default {
},
props: {
row: Object,
fields: Array,
fields: {
type: Array,
default: () => []
},
keyUsage: Array
},
data () {
@@ -250,13 +257,18 @@ 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;
}
},
created () {
this.fields.forEach(field => {
this.isInlineEditor[field.name] = false;
});
watch: {
fields () {
this.fields.forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
},
methods: {
getFieldType (cKey) {
@@ -265,7 +277,7 @@ export default {
if (field)
type = field.type;
return type;
return type.toLowerCase();
},
getFieldPrecision (cKey) {
let length = 0;
@@ -276,7 +288,24 @@ export default {
return length;
},
getFieldObj (cKey) {
return this.fields.filter(field => field.name === cKey || 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' : '';
@@ -285,7 +314,9 @@ export default {
return bufferToBase64(val);
},
editON (event, content, field) {
const type = this.getFieldType(field);
if (!this.isEditable) return;
const type = this.getFieldType(field).toUpperCase(); ;
this.originalContent = content;
this.editingType = type;
this.editingField = field;
@@ -325,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 () {
@@ -394,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,19 +1,39 @@
<template>
<div class="workspace-query-tab column col-12 columns col-gapless">
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<div class="dropdown">
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0 pr-1"
:class="{'loading':isQuering}"
title="F5"
@click="reloadTable"
>
<span>{{ $t('word.refresh') }}</span>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh ml-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h ml-1" />
</button>
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
<i class="mdi mdi-24px mdi-menu-down" />
</div>
<div class="menu px-3">
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
<input
v-model="autorefreshTimer"
class="slider no-border"
type="range"
min="0"
max="30"
step="1"
@change="setRefreshInterval"
>
</div>
</div>
</div>
<button
class="btn btn-link btn-sm"
:class="{'loading':isQuering}"
@click="reloadTable"
>
<span>{{ $t('word.refresh') }}</span>
<i class="mdi mdi-24px mdi-refresh ml-1" />
</button>
<button
class="btn btn-link btn-sm"
:class="{'disabled':isQuering}"
class="btn btn-dark btn-sm"
@click="showAddModal"
>
<span>{{ $t('word.add') }}</span>
@@ -21,8 +41,11 @@
</button>
</div>
<div class="workspace-query-info">
<div v-if="results.rows">
{{ $t('word.results') }}: <b>{{ results.rows.length }}</b>
<div v-if="results.length && results[0].rows">
{{ $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>
@@ -36,12 +59,17 @@
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:conn-uid="connection.uid"
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"
@@ -71,11 +99,12 @@ export default {
return {
tabUid: 'data',
isQuering: false,
results: {},
fields: [],
keyUsage: [],
results: [],
lastTable: null,
isAddModal: false
isAddModal: false,
autorefreshTimer: 0,
refreshInterval: null,
sortParams: {}
};
},
computed: {
@@ -87,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) {
@@ -105,56 +150,36 @@ export default {
},
created () {
this.getTableData();
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
clearInterval(this.refreshInterval);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
setTabFields: 'workspaces/setTabFields',
setTabKeyUsage: 'workspaces/setTabKeyUsage'
addNotification: 'notifications/addNotification'
}),
async getTableData () {
async getTableData (sortParams) {
if (!this.table) return;
this.isQuering = true;
this.results = {};
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
// if table changes clear cached values
if (this.lastTable !== this.table)
this.results = [];
const params = {
uid: this.connection.uid,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table
schema: this.schema,
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
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: 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);
if (status === 'success')
this.results = 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
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: response });
}
this.results = [response];
else
this.addNotification({ status: 'error', message: response });
}
@@ -164,50 +189,40 @@ export default {
this.isQuering = false;
},
getTable () {
return this.table;
},
reloadTable () {
this.getTableData();
this.getTableData(this.sortParams);
},
hardSort (sortParams) {
this.sortParams = sortParams;
this.getTableData(sortParams);
},
showAddModal () {
this.isAddModal = true;
},
hideAddModal () {
this.isAddModal = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.key === 'F5')
this.reloadTable();
}
},
setRefreshInterval () {
if (this.refreshInterval)
clearInterval(this.refreshInterval);
if (+this.autorefreshTimer) {
this.refreshInterval = setInterval(() => {
if (!this.isQuering)
this.reloadTable();
}, this.autorefreshTimer * 1000);
}
}
}
};
</script>
<style lang="scss">
.workspace-tabs {
align-content: baseline;
.workspace-query-runner {
.workspace-query-runner-footer {
display: flex;
justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem;
align-items: center;
.workspace-query-buttons {
display: flex;
.btn {
display: flex;
align-self: center;
color: $body-font-color;
margin-right: 0.4rem;
}
}
.workspace-query-info {
display: flex;
> div + div {
padding-left: 0.6rem;
}
}
}
}
}
</style>

View File

@@ -50,7 +50,7 @@ module.exports = {
testConnection: 'إختبر الإتصال',
editConnection: 'عدل الإتصال',
deleteConnection: 'إحذف الإتصال',
deleteConnectionCorfirm: 'هل أنت متأكد من حذف الإتصال؟',
deleteCorfirm: 'هل أنت متأكد من حذف الإتصال؟',
connectionSuccessfullyMade: 'تم الإتصال بنجاح!',
madeWithJS: 'بني بـ 💛 و جافاسكربت!',
checkForUpdates: 'تأكد من التحديثات',

View File

@@ -38,7 +38,36 @@ module.exports = {
add: 'Add',
data: 'Data',
properties: 'Properties',
insert: 'Insert'
insert: 'Insert',
connecting: 'Connecting',
name: 'Name',
collation: 'Collation',
clear: 'Clear',
options: 'Options',
autoRefresh: 'Auto-refresh',
indexes: 'Indexes',
foreignKeys: 'Foreign keys',
length: 'Length',
unsigned: 'Unsigned',
default: 'Default',
comment: 'Comment',
key: 'Key | Keys',
order: 'Order',
expression: 'Expression',
autoIncrement: 'Auto Increment',
engine: 'Engine',
field: 'Field | Fields',
approximately: 'Approximately',
total: 'Total',
table: 'Table',
discard: 'Discard',
stay: 'Stay',
author: 'Author',
light: 'Light',
dark: 'Dark',
autoCompletion: 'Auto Completion',
application: 'Application',
editor: 'Editor'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -50,7 +79,7 @@ module.exports = {
testConnection: 'Test connection',
editConnection: 'Edit connection',
deleteConnection: 'Delete connection',
deleteConnectionCorfirm: 'Do you confirm the cancellation of',
deleteCorfirm: 'Do you confirm the cancellation of',
connectionSuccessfullyMade: 'Connection successfully made!',
madeWithJS: 'Made with 💛 and JavaScript!',
checkForUpdates: 'Check for updates',
@@ -70,7 +99,39 @@ module.exports = {
addNewRow: 'Add new row',
numberOfInserts: 'Number of inserts',
openNewTab: 'Open a new tab',
affectedRows: 'Affected rows'
affectedRows: 'Affected rows',
createNewDatabase: 'Create new Database',
databaseName: 'Database name',
serverDefault: 'Server default',
deleteDatabase: 'Delete 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',
applicationTheme: 'Application Theme',
editorTheme: 'Editor Theme'
},
// Date and Time
short: {

View File

@@ -0,0 +1,90 @@
module.exports = {
word: {
edit: 'Editar',
save: 'Guardar',
close: 'Cerrar',
delete: 'Eliminar',
confirm: 'Confirmar',
cancel: 'Cancelar',
send: 'Enviar',
connectionName: 'Nombre de la conexión',
client: 'Cliente',
hostName: 'Servidor',
port: 'Puerto',
user: 'Usuario',
password: 'Contraseña',
credentials: 'Credenciales',
connect: 'Connectar',
connected: 'Conectado',
disconnect: 'Desconectar',
disconnected: 'Desconectado',
refresh: 'Refrescar',
settings: 'Configuración',
general: 'General',
themes: 'Temas',
update: 'Actualizar',
about: 'Sobre',
language: 'Idioma',
version: 'Versión',
donate: 'Donar',
run: 'Ejecutar',
schema: 'Esquema',
results: 'Resultados',
size: 'Tamaño',
seconds: 'Segundos',
type: 'Tipo',
mimeType: 'Mime-Type',
download: 'Descargar',
add: 'Añadir',
data: 'Datos',
properties: 'Propiedades',
insert: 'Insertar',
connecting: 'Conectando'
},
message: {
appWelcome: 'Bienvenido a Antares Cliente SQL!',
appFirstStep: 'Primer paso: Crear una conexión a una Base de Datos.',
addConnection: 'Añadir conexión',
createConnection: 'Crear conexión',
createNewConnection: 'Crear nueva conexión',
askCredentials: 'Preguntar credenciales',
testConnection: 'Comprobar conexión',
editConnection: 'Editar conexión',
deleteConnection: 'Eliminar conexión',
deleteCorfirm: 'Confirmas la cancelación de',
connectionSuccessfullyMade: 'Conexión realizada correctamente!',
madeWithJS: 'Hecho con 💛 y JavaScript!',
checkForUpdates: 'Comprobar actualizaciones',
noUpdatesAvailable: 'No hay actualizaciones',
checkingForUpdate: 'Comprobando actualizaciones',
checkFailure: 'Error en la comprobación, por favor pruebe más tarde',
updateAvailable: 'Actualización disponible',
downloadingUpdate: 'Descargando actualización',
updateDownloaded: 'Descargada actualización',
restartToInstall: 'Reiniciar Antares para instalar',
unableEditFieldWithoutPrimary: 'No se puede editar una campo sin Llave Primaria en el registro',
editCell: 'Editar celda',
deleteRows: 'Eliminar fila | Eliminar {count} filas',
confirmToDeleteRows: '¿Quiere realmente eliminar una fila? | ¿Quiere realmente eliminar {count} filas?',
notificationsTimeout: 'Tiempo de espera',
uploadFile: 'Cargar fichero',
addNewRow: 'Añadir nueva fila',
numberOfInserts: 'Numero de inserciones',
openNewTab: 'Abrir nueva pestaña',
affectedRows: 'Filas afectadas'
},
// Date and Time
short: {
year: 'numeric',
month: 'short',
day: 'numeric'
},
long: {
year: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: 'numeric'
}
};

View File

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

View File

@@ -29,7 +29,17 @@ module.exports = {
donate: 'Dona',
run: 'Esegui',
schema: 'Schema',
results: 'Results'
results: 'Risultati',
size: 'Dimensioni',
seconds: 'Secondi',
type: 'Tipo',
mimeType: 'Mime-Type',
download: 'Scarica',
add: 'Aggiungi',
data: 'Dati',
properties: 'Proprietà',
insert: 'Inserisci',
connecting: 'Connessione in corso'
},
message: {
appWelcome: 'Benvenuto in Antares SQL Client!',
@@ -41,7 +51,7 @@ module.exports = {
testConnection: 'Testa connessione',
editConnection: 'Modifica connessione',
deleteConnection: 'Elimina connessione',
deleteConnectionCorfirm: 'Confermi l\'eliminazione di',
deleteCorfirm: 'Confermi l\'eliminazione di',
connectionSuccessfullyMade: 'Connessione avvenuta con successo!',
madeWithJS: 'Fatto con 💛 e JavaScript!',
checkForUpdates: 'Cerca aggiornamenti',
@@ -55,7 +65,13 @@ module.exports = {
unableEditFieldWithoutPrimary: 'Impossibile modificare il campo senza una primary key nel resultset',
editCell: 'Modifica cella',
deleteRows: 'Elimina riga | Elimina {count} righe',
confirmToDeleteRows: 'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?'
confirmToDeleteRows: 'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?',
notificationsTimeout: 'Timeout Notifiche',
uploadFile: 'Carica file',
addNewRow: 'Aggiungi nuova riga',
numberOfInserts: 'Numero di insert',
openNewTab: 'Apri nuova scheda',
affectedRows: 'Righe interessate'
},
// Date and Time
short: {

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 210" height="210mm" width="210mm"><path d="M180.088 107.51a72.967 69.474 0 01-72.858 69.473 72.967 69.474 0 01-73.075-69.266 72.967 69.474 0 0172.639-69.68 72.967 69.474 0 0173.292 69.056" fill="#e36929"/><path d="M82.985 102.48a17.02 21.735 21.455 01-23.767 14.012 17.02 21.735 21.455 01-7.938-26.403 17.02 21.735 21.455 0123.744-14.091 17.02 21.735 21.455 018.009 26.36M157.83 118.562a17.472 11.51-52.488 01-1.695 20.917 17.472 11.51-52.488 01-19.742 6.678 17.472 11.51-52.488 011.635-20.897 17.472 11.51-52.488 0119.747-6.74" fill="#f8d163"/><path d="M72.979 98.717a9.143 10.562 0 01-9.13 10.562 9.143 10.562 0 01-9.156-10.53 9.143 10.562 0 019.102-10.594 9.143 10.562 0 019.184 10.499M155.659 130.732a6.21 6.986 0 01-6.2 6.987 6.21 6.986 0 01-6.22-6.966 6.21 6.986 0 016.182-7.007 6.21 6.986 0 016.238 6.945" fill="#fbfcf7"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 210">
<defs/>
<path fill="#e36929" d="M180.088 107.51a72.967 69.474 0 01-72.858 69.473 72.967 69.474 0 01-73.075-69.266 72.967 69.474 0 0172.639-69.68 72.967 69.474 0 0173.292 69.056"/>
<path fill="#f8d163" d="M82.985 102.48a17.02 21.735 21.455 01-23.767 14.012 17.02 21.735 21.455 01-7.938-26.403 17.02 21.735 21.455 0123.744-14.091 17.02 21.735 21.455 018.009 26.36m74.797 16.204a17.472 11.51-52.488 01-1.695 20.917 17.472 11.51-52.488 01-19.742 6.678 17.472 11.51-52.488 011.635-20.897 17.472 11.51-52.488 0119.747-6.74"/>
<path fill="#fbfcf7" d="M72.979 98.717a9.143 10.562 0 01-9.13 10.562 9.143 10.562 0 01-9.156-10.53 9.143 10.562 0 019.102-10.594 9.143 10.562 0 019.184 10.499m82.68 32.078a6.21 6.986 0 01-6.2 6.987 6.21 6.986 0 01-6.22-6.966 6.21 6.986 0 016.182-7.007 6.21 6.986 0 016.238 6.945"/>
</svg>

Before

Width:  |  Height:  |  Size: 892 B

After

Width:  |  Height:  |  Size: 880 B

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

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

View File

@@ -17,12 +17,4 @@ export default class {
static disconnect (uid) {
return ipcRenderer.invoke('disconnect', uid);
}
static refresh (uid) {
return ipcRenderer.invoke('refresh', uid);
}
static rawQuery (params) {
return ipcRenderer.invoke('raw-query', params);
}
}

View File

@@ -0,0 +1,44 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static createDatabase (params) {
return ipcRenderer.invoke('create-database', params);
}
static updateDatabase (params) {
return ipcRenderer.invoke('update-database', params);
}
static getDatabaseCollation (params) {
return ipcRenderer.invoke('get-database-collation', params);
}
static deleteDatabase (params) {
return ipcRenderer.invoke('delete-database', params);
}
static getStructure (uid) {
return ipcRenderer.invoke('get-structure', uid);
}
static getCollations (uid) {
return ipcRenderer.invoke('get-collations', uid);
}
static getVariables (uid) {
return ipcRenderer.invoke('get-variables', uid);
}
static getEngines (uid) {
return ipcRenderer.invoke('get-engines', uid);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}
static rawQuery (params) {
return ipcRenderer.invoke('raw-query', 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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,17 @@
import Tables from '@/ipc-api/Tables';
export default {
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
}
},
methods: {
async updateField (payload) {
this.isQuering = true;
const params = {
uid: this.connection.uid,
schema: this.workspace.breadcrumbs.schema,
table: this.table,
...payload
};
@@ -34,18 +37,14 @@ export default {
const params = {
uid: this.connection.uid,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table,
...payload
};
try {
const { status, response } = await Tables.deleteTableRows(params);
if (status === 'success') {
const { primary, rows } = params;
this.results = { ...this.results, rows: this.results.rows.filter(row => !rows.includes(row[primary])) };
this.$refs.queryTable.refreshScroller();// Necessary to re-render virtual scroller
}
if (status === 'success')
this.reloadTable();
else
this.addNotification({ status: 'error', message: response });
}

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

@@ -1,18 +1,25 @@
.column-key {
transform: rotate(90deg);
transform: rotate(45deg);
font-size: 0.7rem;
line-height: 1.5;
margin-right: 0.2rem;
&.key-pri {
&.key-pri,
&.key-PRIMARY {
color: goldenrod;
}
&.key-uni {
&.key-uni,
&.key-UNIQUE {
color: deepskyblue;
}
&.key-mul {
&.key-mul,
&.key-INDEX {
color: palegreen;
}
&.key-FULLTEXT {
color: mediumvioletred;
}
}

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

@@ -19,12 +19,24 @@ body {
@include padding-variant(3, $unit-3);
@include padding-variant(4, $unit-4);
.btn.btn-gray {
color: #fff;
background: $bg-color-gray;
.btn {
&.btn-gray {
color: #fff;
background: $bg-color-gray;
&:hover {
background: $bg-color;
&:hover {
background: $bg-color;
}
}
&.btn-dark {
color: #fff;
background: $bg-color-light;
border-color: $bg-color-light;
&:hover {
background: $bg-color-gray;
}
}
}
@@ -37,6 +49,12 @@ body {
cursor: help;
}
.no-border {
outline: none !important;
border: none !important;
box-shadow: none !important;
}
.bg-checkered {
background-image:
linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
@@ -46,6 +64,38 @@ body {
background-size: 2em 2em;
}
.workspace-tabs {
align-content: baseline;
.workspace-query-runner {
.workspace-query-runner-footer {
display: flex;
justify-content: space-between;
padding: 0.3rem 0.6rem 0.4rem;
align-items: center;
.workspace-query-buttons {
display: flex;
.btn {
display: flex;
align-self: center;
color: $body-font-color;
margin-right: 0.4rem;
}
}
.workspace-query-info {
display: flex;
> div + div {
padding-left: 0.6rem;
}
}
}
}
}
// Scrollbars
::-webkit-scrollbar {
width: 10px;
@@ -130,6 +180,12 @@ body {
.form-select {
cursor: pointer;
&.small-select {
height: 1rem;
font-size: 0.7rem;
padding: 1px 0.4rem 0;
}
}
.form-select,
@@ -141,6 +197,11 @@ body {
background-color: $bg-color-gray;
}
.form-input.is-error,
.form-select.is-error {
background-color: $bg-color-gray;
}
.form-input:not(:placeholder-shown):invalid:focus {
background: $bg-color-gray;
}
@@ -173,3 +234,7 @@ body {
visibility: hidden;
}
}
.empty {
color: $body-font-color;
}

View File

@@ -3,11 +3,10 @@ export default {
namespaced: true,
strict: true,
state: {
app_name: 'Antares - Database Client',
app_name: 'Antares - SQL Client',
app_version: process.env.PACKAGE_VERSION || 0,
is_loading: false,
is_new_modal: false,
is_edit_modal: false,
is_setting_modal: false,
selected_setting_tab: 'general',
selected_conection: {},
@@ -20,7 +19,6 @@ export default {
appVersion: state => state.app_version,
getSelectedConnection: state => state.selected_conection,
isNewModal: state => state.is_new_modal,
isEditModal: state => state.is_edit_modal,
isSettingModal: state => state.is_setting_modal,
selectedSettingTab: state => state.selected_setting_tab,
getUpdateStatus: state => state.update_status,
@@ -36,13 +34,6 @@ export default {
HIDE_NEW_CONNECTION_MODAL (state) {
state.is_new_modal = false;
},
SHOW_EDIT_CONNECTION_MODAL (state, connection) {
state.is_edit_modal = true;
state.selected_conection = connection;
},
HIDE_EDIT_CONNECTION_MODAL (state) {
state.is_edit_modal = false;
},
SHOW_SETTING_MODAL (state, tab) {
state.selected_setting_tab = tab;
state.is_setting_modal = true;
@@ -68,12 +59,6 @@ export default {
hideNewConnModal ({ commit }) {
commit('HIDE_NEW_CONNECTION_MODAL');
},
showEditConnModal ({ commit }, connection) {
commit('SHOW_EDIT_CONNECTION_MODAL', connection);
},
hideEditConnModal ({ commit }) {
commit('HIDE_EDIT_CONNECTION_MODAL');
},
showSettingModal ({ commit }, tab) {
commit('SHOW_SETTING_MODAL', tab);
},

View File

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

View File

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

View File

@@ -1,26 +1,19 @@
'use strict';
import Connection from '@/ipc-api/Connection';
import Database from '@/ipc-api/Database';
import { uidGen } from 'common/libs/uidGen';
const tabIndex = [];
function remapStructure (structure) { // TODO: move to main process and add fields (for autocomplete purpose)
const databases = structure.map(table => table.TABLE_SCHEMA)
.filter((value, index, self) => self.indexOf(value) === index);
return databases.map(db => {
return {
name: db,
tables: structure.filter(table => table.TABLE_SCHEMA === db)
};
});
}
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 => {
@@ -31,6 +24,9 @@ export default {
getWorkspace: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid);
},
getDatabaseVariable: state => (uid, name) => {
return state.workspaces.find(workspace => workspace.uid === uid).variables.find(variable => variable.name === name);
},
getWorkspaceTab: (state, getters) => tUid => {
if (!getters.getSelected) return;
const workspace = state.workspaces.find(workspace => workspace.uid === getters.getSelected);
@@ -42,32 +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);
},
REFRESH_VARIABLES (state, { uid, variables }) {
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',
@@ -133,6 +180,15 @@ 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: {
@@ -144,8 +200,28 @@ export default {
const { status, response } = await Connection.connect(connection);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('ADD_CONNECTED', { uid: connection.uid, structure: remapStructure(response) });
else {
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) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
@@ -153,11 +229,47 @@ export default {
},
async refreshStructure ({ dispatch, commit }, uid) {
try {
const { status, response } = await Connection.refresh(uid);
const { status, response } = await Database.getStructure(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('REFRESH_STRUCTURE', { uid, structure: remapStructure(response) });
commit('REFRESH_STRUCTURE', { uid, structure: response });
}
catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshCollations ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getCollations(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('REFRESH_COLLATIONS', { uid, collations: response });
}
catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
}
},
async refreshVariables ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getVariables(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
commit('REFRESH_VARIABLES', { uid, variables: response });
}
catch (err) {
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 });
@@ -186,6 +298,8 @@ export default {
keyUsage: []
}],
structure: {},
variables: [],
collations: [],
breadcrumbs: {}
};
@@ -193,12 +307,40 @@ export default {
if (getters.getWorkspace(uid).tabs.length < 3)
dispatch('newTab', uid);
dispatch('setUnsavedChanges', false);
},
changeBreadcrumbs ({ commit, getters }, payload) {
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: payload });
changeBreadcrumbs ({ state, commit, getters }, payload) {
if (state.has_unsaved_changes) {
commit('SET_UNSAVED_DISCARD_MODAL', true);
commit('SET_PENDING_BREADCRUMBS', payload);
return;
}
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);
@@ -211,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);
}
}
};

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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