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

Compare commits

...

82 Commits

Author SHA1 Message Date
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
77 changed files with 3973 additions and 1024 deletions

View File

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

1
.gitattributes vendored Normal file
View File

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

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.

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

@@ -0,0 +1,62 @@
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
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- 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

@@ -22,6 +22,7 @@ jobs:
- stage: Test
script:
- npm test
- stage: Deploy Linux & Windows
if: tag IS present
os: linux
@@ -30,9 +31,17 @@ jobs:
- docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always"
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
- stage: Deploy Mac
if: tag IS present
os: osx
osx_image: xcode10.2
script:
- npm run build -- -p always
- npm run build -- -p always
- stage: Deploy ARM Linux
if: tag IS present
os: linux
arch: arm64
script:
- npm run build -- --linux AppImage -p always

View File

@@ -2,24 +2,96 @@
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.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

@@ -9,35 +9,53 @@
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 isn't ready as a main SQL client**. However i'm actively working on it (yes, i'm a lone dev), hoping to provide all essential features as soon as possible.
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/EStarium/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)
## Roadmap
## Current main features
- Multiple database connections at same time.
- Database management (add/edit/delete).
- Tables fields management (add/edit/delete).
- Tables content management (add/edit/delete).
- Run queries on multiple tabs.
- Query suggestions.
- Native dark theme.
- Multi language.
- Auto updates.
## Coming soon
This is a roadmap with major features will come in near future.
- Improvements of query editor area.
- Multiple query tabs.
- Tables management (add/edit/delete).
- Stored procedures, views, schedulers and trigger support.
- Database tools.
- Context menu shortcuts.
- Keyboard shortcuts.
- Users management (add/edit/delete).
- Stored procedures, views, schedulers and triggers support.
- More secure password storage.
- Database tools (variables, process list...).
- Support for other databases.
- Improvements of query editor area.
- Improvements of query suggestions.
- Query history.
- More context menu shortcuts.
- More keyboard shortcuts.
- Query logs console.
- Fake data filler.
- Import/export and migration.
- SSL and SSH tunnel support.
- Themes.
## Currently supported
@@ -45,7 +63,7 @@ This is a roadmap with major features will come in near future.
### Databases
- [x] MySQL/MariaDB
- [ ] PostrgreSQL
- [ ] PostgreSQL
- [ ] MSSQL
- [ ] SQLite
- [ ] OracleDB
@@ -53,7 +71,7 @@ This is a roadmap with major features will come in near future.
### Operating Systems
#### • x86
#### • x64
- [x] Windows
- [x] Linux
@@ -62,10 +80,11 @@ This is a roadmap with major features will come in near future.
#### • ARM
- [ ] Windows
- [ ] Linux
- [x] Linux
- [ ] MacOS
## 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)
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/EStarium/antares/pull/29)
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/EStarium/antares/pull/32)

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

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.0.6",
"version": "0.0.9",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/EStarium/antares.git",
@@ -11,7 +11,9 @@
"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": {
@@ -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",
"electron-log": "^4.3.0",
"electron-updater": "^4.3.5",
"lodash": "^4.17.20",
"moment": "^2.27.0",
"moment": "^2.29.1",
"monaco-editor": "^0.20.0",
"mssql": "^6.2.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.1",
"vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.0",
"vuedraggable": "^2.24.3",
"vuex": "^3.5.1",
"vuex-persist": "^2.2.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": "^10.1.5",
"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.13.0",
"eslint-config-standard": "^16.0.1",
"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-standard": "^4.1.0",
"eslint-plugin-vue": "^7.1.0",
"monaco-editor-webpack-plugin": "^1.9.1",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.0",
"standard-version": "^9.0.0",
"stylelint": "^13.6.1",
"stylelint": "^13.7.2",
"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: true,
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,7 +1,7 @@
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'];

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

View File

@@ -6,6 +6,8 @@ import { format as formatUrl } from 'url';
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 +69,27 @@ 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)
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();
});
// create main BrowserWindow when electron is ready
app.on('ready', () => {
mainWindow = createMainWindow();
});
});
}

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,110 @@
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('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) {
@@ -17,7 +16,13 @@ export default (connections) => {
ipcMain.handle('get-table-data', async (event, { uid, schema, table }) => {
try {
const result = await Tables.getTableData(connections[uid], schema, table);
const result = await connections[uid]
.select('*')
.schema(schema)
.from(table)
.limit(1000)
.run({ details: true });
return { status: 'success', response: result };
}
catch (err) {
@@ -25,9 +30,10 @@ export default (connections) => {
}
});
ipcMain.handle('get-key-usage', async (event, { uid, schema, table }) => {
ipcMain.handle('get-key-usage', async (event, params) => {
try {
const result = await InformationSchema.getKeyUsage(connections[uid], schema, table);
const result = await connections[params.uid].getKeyUsage(params);
return { status: 'success', response: result };
}
catch (err) {
@@ -37,8 +43,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() };
@@ -47,7 +79,12 @@ export default (connections) => {
ipcMain.handle('delete-table-rows', async (event, params) => {
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 (${params.rows.join(',')})` })
.run();
return { status: 'success', response: result };
}
catch (err) {
@@ -57,7 +94,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 +134,33 @@ 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('alter-table', async (event, params) => {
try {
await connections[params.uid].alterTable(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,521 @@
'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,
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 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();
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
};
});
}
/**
* 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
};
});
}
/**
* ALTER TABLE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterTable (params) {
const {
table,
additions,
deletions,
changes
} = params;
let sql = `ALTER TABLE \`${table}\` `;
const alterColumns = [];
// ADD
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'}`);
});
// CHANGE
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'}`);
});
// DROP
deletions.forEach(deletion => {
alterColumns.push(`DROP COLUMN \`${deletion.name}\``);
});
sql += alterColumns.join(', ');
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,7 +16,6 @@
<TheFooter />
<TheNotificationsBoard />
<ModalNewConnection v-if="isNewConnModal" />
<ModalEditConnection v-if="isEditModal" />
<ModalSettings v-if="isSettingModal" />
</div>
</div>
@@ -36,7 +35,6 @@ 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')
},
data () {

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,7 +83,6 @@ export default {
top: 0;
left: 0;
bottom: 0;
pointer-events: none;
.context-container {
min-width: 100px;
@@ -72,6 +102,7 @@ export default {
align-items: center;
padding: 0.1rem 0.3rem;
cursor: pointer;
justify-content: space-between;
&:hover {
background: $primary-color;

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

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">
@@ -113,13 +113,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 +144,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 +155,9 @@ export default {
ModalAskCredentials,
BaseToast
},
props: {
connection: Object
},
data () {
return {
toast: {
@@ -166,17 +169,15 @@ export default {
localConnection: null
};
},
computed: {
...mapGetters({
connection: 'application/getSelectedConnection'
})
},
created () {
this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
closeModal: 'application/hideEditConnModal',
editConnection: 'connections/editConnection'
}),
async startTest () {
@@ -226,6 +227,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,161 @@
<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 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);
},
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">
@@ -117,13 +117,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 +180,12 @@ export default {
isAsking: false
};
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
closeModal: 'application/hideNewConnModal',
@@ -249,6 +255,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,134 @@
<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
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);
},
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

@@ -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">
@@ -124,8 +124,9 @@ export default {
}
},
props: {
connection: Object,
tabUid: [String, Number]
tabUid: [String, Number],
fields: Array,
keyUsage: Array
},
data () {
return {
@@ -146,12 +147,6 @@ export default {
},
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 : [];
}
},
watch: {
@@ -160,6 +155,9 @@ export default {
this.nInserts = 1;
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
const rowObj = {};
@@ -195,6 +193,9 @@ export default {
this.localRow = { ...rowObj };
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
@@ -242,7 +243,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 +295,13 @@ export default {
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};

View File

@@ -164,6 +164,10 @@ export default {
this.localLocale = this.selectedLocale;
this.localTimeout = this.notificationsTimeout;
this.selectedTab = this.selectedSettingTab;
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
@@ -182,6 +186,11 @@ export default {
this.localTimeout = 10;
this.updateNotificationsTimeout(+this.localTimeout);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};

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

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

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,11 @@
</a>
</li>
</ul>
<WorkspacePropsTab
v-show="selectedTab === 'prop'"
:connection="connection"
:table="workspace.breadcrumbs.table"
/>
<WorkspaceTableTab
v-show="selectedTab === 'data'"
:connection="connection"
@@ -77,17 +92,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',
@@ -131,6 +153,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;
@@ -197,6 +227,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,9 +38,22 @@
:key="db.name"
:database="db"
:connection="connection"
@show-database-context="openDatabaseContext"
/>
</div>
</div>
<ModalNewDatabase
v-if="isNewDBModal"
@close="hideNewDBModal"
@reload="refresh"
/>
<DatabaseContext
v-if="isDatabaseContext"
:selected-database="selectedDatabase"
:context-event="databaseContextEvent"
@close-context="closeDatabaseContext"
@reload="refresh"
/>
</div>
</template>
@@ -44,12 +62,16 @@ import { mapGetters, mapActions } from 'vuex';
import _ from 'lodash';
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase';
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext';
import ModalNewDatabase from '@/components/ModalNewDatabase';
export default {
name: 'WorkspaceExploreBar',
components: {
WorkspaceConnectPanel,
WorkspaceExploreBarDatabase
WorkspaceExploreBarDatabase,
DatabaseContext,
ModalNewDatabase
},
props: {
connection: Object,
@@ -58,7 +80,14 @@ export default {
data () {
return {
isRefreshing: false,
localWidth: null
isNewDBModal: false,
localWidth: null,
isDatabaseContext: false,
isTableContext: false,
databaseContextEvent: null,
tableContextEvent: null,
selectedDatabase: '',
selectedTable: ''
};
},
computed: {
@@ -117,6 +146,22 @@ export default {
},
stopResize () {
window.removeEventListener('mousemove', this.resize);
},
showNewDBModal () {
this.isNewDBModal = true;
},
hideNewDBModal () {
this.isNewDBModal = false;
},
openDatabaseContext (payload) {
this.isTableContext = false;
this.selectedDatabase = payload.database;
this.databaseContextEvent = payload.event;
this.isDatabaseContext = true;
},
closeDatabaseContext () {
this.isDatabaseContext = false;
this.selectedDatabase = '';
}
}
};

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="changeBreadcrumbs({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,117 @@ export default {
}),
breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs;
},
maxSize () {
return this.database.tables.reduce((acc, curr) => {
if (curr.size > acc) acc = curr.size;
return acc;
}, 0);
},
totalSize () {
return this.database.tables.reduce((acc, curr) => acc + curr.size, 0);
}
},
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
})
}),
formatBytes,
showDatabaseContext (event, database) {
this.$emit('show-database-context', { event, database });
},
showTableContext (table) {
this.$emit('show-table-context', table);
},
piePercentage (val) {
const perc = val / this.maxSize * 100;
return { background: `conic-gradient(lime ${perc}%, white 0)` };
}
}
};
</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,112 @@
<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> -->
<div class="context-element" @click="showEditModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span>
</div>
<div class="context-element" @click="showDeleteModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}</span>
</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'
})
},
methods: {
...mapActions({
deleteConnection: 'connections/deleteConnection',
showEditModal: 'application/showEditConnModal',
addNotification: 'notifications/addNotification'
}),
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') {
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,262 @@
<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 d-none">
<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 d-none">
<span>{{ $t('word.foreignKeys') }}</span>
<i class="mdi mdi-24px mdi-key-link ml-1" />
</button>
<button class="btn btn-dark btn-sm d-none">
<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="queryTable"
:fields="localFields"
:tab-uid="tabUid"
:conn-uid="connection.uid"
:table="table"
:schema="schema"
mode="table"
@remove-field="removeField"
/>
</div>
</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';
export default {
name: 'WorkspacePropsTab',
components: {
WorkspacePropsTable
},
props: {
connection: Object,
table: String
},
data () {
return {
tabUid: 'prop',
isQuering: false,
isSaving: false,
originalFields: [],
localFields: [],
originalKeyUsage: [],
localKeyUsage: [],
lastTable: null,
isAddModal: false
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
},
schema () {
return this.workspace.breadcrumbs.schema;
},
isChanged () {
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) || JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage);
}
},
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;
}
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async getFieldsData () {
if (!this.table) return;
this.isQuering = true;
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 { // Key usage (foreign keys)
const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success') {
this.originalKeyUsage = response;
this.localKeyUsage = JSON.parse(JSON.stringify(response));
}
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;
const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._id], []);
const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._id], []);
// 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 };
});
// Deletions
const deletions = this.originalFields.filter(field => !localIDs.includes(field._id));
// 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 });
});
const params = {
uid: this.connection.uid,
schema: this.schema,
table: this.workspace.breadcrumbs.table,
additions,
changes,
deletions
};
try { // Key usage (foreign keys)
const { status, response } = await Tables.alterTable(params);
if (status === 'success')
this.getFieldsData();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localFields = JSON.parse(JSON.stringify(this.originalFields));
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
},
addField () {
this.localFields.push({
_id: uidGen(),
name: '',
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: ''
});
},
removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid);
},
showAddModal () {
this.isAddModal = true;
},
hideAddModal () {
this.isAddModal = false;
}
}
};
</script>

View File

@@ -0,0 +1,211 @@
<template>
<div
ref="tableWrapper"
class="vscroll"
:style="{'height': resultsSize+'px'}"
>
<TableContext
v-if="isContext"
:context-event="contextEvent"
:selected-field="selectedField"
@delete-selected="removeField"
@close-context="isContext = false"
/>
<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">
<div class="table-column-title">
{{ $t('word.name') }}
</div>
</div>
</div>
<div class="th">
<div class="column-resizable">
<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">
<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"
: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,
tabUid: [String, Number],
connUid: String,
table: String,
schema: String,
mode: String
},
data () {
return {
resultsSize: 1000,
localResults: [],
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);
}
},
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;
}
// this.$refs.resultTable.updateWindow();
}
},
refreshScroller () {
this.resizeResults();
},
contextMenu (event, uid) {
this.selectedField = uid;
this.contextEvent = event;
this.isContext = true;
},
removeField () {
this.$emit('remove-field', this.selectedField);
}
}
};
</script>
<style lang="scss" scoped>
.column-resizable {
&:hover,
&:active {
resize: horizontal;
overflow: hidden;
}
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<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,
selectedField: String
},
data () {
return {
isConfirmModal: false
};
},
computed: {
},
methods: {
closeContext () {
this.$emit('close-context');
},
deleteField () {
this.$emit('delete-selected');
this.closeContext();
}
}
};
</script>

View File

@@ -0,0 +1,516 @@
<template>
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)">
<div class="td">
<div class="row-draggable">
<i class="mdi mdi-drag-horizontal row-draggable-icon" />
{{ localRow.order }}
</div>
</div>
<div class="td" tabindex="0">
<i
v-if="localRow.key"
:title="keyName(localRow.key)"
class="mdi mdi-key column-key c-help pl-1"
:class="`key-${localRow.key}`"
/>
</div>
<div class="td">
<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 text-left" :class="`type-${lowerCase(localRow.type)}`">
<span
v-if="!isInlineEditor.type"
class="cell-content"
@dblclick="editON($event, localRow.type.toUpperCase(), 'type')"
>
{{ localRow.type }}
</span>
<select
v-else
ref="editField"
v-model="editingContent"
class="editable-field px-1 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">
<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">
<label class="form-checkbox">
<input
v-model="localRow.unsigned"
type="checkbox"
:disabled="!fieldType.unsigned"
>
<i class="form-icon" />
</label>
</div>
<div class="td">
<label class="form-checkbox">
<input
v-model="localRow.nullable"
type="checkbox"
:disabled="localRow.key === 'pri'"
>
<i class="form-icon" />
</label>
</div>
<div class="td">
<label class="form-checkbox">
<input
v-model="localRow.zerofill"
type="checkbox"
:disabled="!fieldType.zerofill"
>
<i class="form-icon" />
</label>
</div>
<div class="td">
<span class="cell-content" @dblclick="editON($event, localRow.default, 'default')">
{{ fieldDefault }}
</span>
</div>
<div class="td type-varchar">
<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">
<template v-if="fieldType.collation">
<span
v-if="!isInlineEditor.collation"
class="cell-content"
@dblclick="editON($event, localRow.collation, 'collation')"
>
{{ localRow.collation }}
</span>
<select
v-else
ref="editField"
v-model="editingContent"
class="editable-field px-1"
@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="localRow.key !== 'pri'"
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
},
data () {
return {
localRow: {},
isInlineEditor: {},
isDefaultModal: false,
defaultValue: {
type: 'noval',
custom: '',
expression: '',
onUpdate: ''
},
editingContent: 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;
}
},
watch: {
localRow () {
this.initLocalRow();
},
row () {
this.localRow = this.row;
}
},
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;
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.localRow.numLength = false;
this.localRow.charLength = false;
this.localRow.datePrecision = false;
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.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.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

@@ -5,24 +5,29 @@
<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 }}</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 +39,8 @@
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:conn-uid="connection.uid"
mode="query"
@update-field="updateField"
@delete-selected="deleteSelected"
/>
@@ -42,8 +49,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 +72,9 @@ export default {
query: '',
lastQuery: '',
isQuering: false,
results: {},
selectedFields: []
results: [],
resultsCount: 0,
affectedCount: 0
};
},
computed: {
@@ -76,26 +83,20 @@ 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();
@@ -106,57 +107,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 +128,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,13 +108,18 @@ 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;
},
@@ -118,27 +138,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 +190,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 +206,35 @@ 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.resetSort();
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 +256,8 @@ export default {
else {
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id,
...payload
};
@@ -209,19 +268,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 +329,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';
@@ -275,12 +346,15 @@ export default {
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 +379,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

@@ -66,7 +66,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>
@@ -177,6 +177,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 +187,10 @@ export default {
},
props: {
row: Object,
fields: Array,
fields: {
type: Array,
default: () => []
},
keyUsage: Array
},
data () {
@@ -251,12 +255,17 @@ export default {
},
foreignKeys () {
return this.keyUsage.map(key => key.column);
},
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) {
@@ -276,7 +285,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,6 +311,8 @@ export default {
return bufferToBase64(val);
},
editON (event, content, field) {
if (!this.isEditable) return;
const type = this.getFieldType(field);
this.originalContent = content;
this.editingType = type;
@@ -325,9 +353,7 @@ export default {
this.$nextTick(() => document.querySelector('.editable-field').focus());
});
const obj = {
[field]: true
};
const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
},
editOFF () {

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,8 @@
</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 }}</b>
</div>
<div v-if="workspace.breadcrumbs.database">
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b>
@@ -36,12 +56,16 @@
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:conn-uid="connection.uid"
mode="table"
@update-field="updateField"
@delete-selected="deleteSelected"
/>
</div>
<ModalNewTableRow
v-if="isAddModal"
:fields="fields"
:key-usage="keyUsage"
:tab-uid="tabUid"
@hide="hideAddModal"
@reload="reloadTable"
@@ -71,11 +95,11 @@ export default {
return {
tabUid: 'data',
isQuering: false,
results: {},
fields: [],
keyUsage: [],
results: [],
lastTable: null,
isAddModal: false
isAddModal: false,
autorefreshTimer: 0,
refreshInterval: null
};
},
computed: {
@@ -87,6 +111,12 @@ 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 : [];
}
},
watch: {
@@ -105,56 +135,35 @@ 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 () {
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,
schema: this.schema,
table: this.workspace.breadcrumbs.table
};
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,6 +173,9 @@ export default {
this.isQuering = false;
},
getTable () {
return this.table;
},
reloadTable () {
this.getTableData();
},
@@ -172,42 +184,25 @@ export default {
},
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,22 @@ 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'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -50,7 +65,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 +85,21 @@ 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'
},
// 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.

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

@@ -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,40 @@
'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 useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}
static rawQuery (params) {
return ipcRenderer.invoke('raw-query', params);
}
}

View File

@@ -29,4 +29,8 @@ export default class {
static getForeignList (params) {
return ipcRenderer.invoke('get-foreign-list', params);
}
static alterTable (params) {
return ipcRenderer.invoke('alter-table', params);
}
}

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,5 +1,5 @@
.column-key {
transform: rotate(90deg);
transform: rotate(45deg);
font-size: 0.7rem;
line-height: 1.5;
margin-right: 0.2rem;

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;

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

@@ -13,7 +13,7 @@ export default {
return connection.name
? connection.name
: connection.ask
? ''
? `${connection.host}:${connection.port}`
: `${connection.user + '@'}${connection.host}:${connection.port}`;
}
},

View File

@@ -7,7 +7,7 @@ export default {
state: {
locale: 'en-US',
explorebar_size: null,
notifications_timeout: 10
notifications_timeout: 5
},
getters: {
getLocale: state => state.locale,

View File

@@ -1,19 +1,9 @@
'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 lastSchema = '';
export default {
namespaced: true,
@@ -31,6 +21,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);
@@ -48,20 +41,60 @@ export default {
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, structure }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
client,
dataTypes,
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);
},
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) {
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
@@ -144,8 +177,24 @@ 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 = [];
switch (connection.client) {
case 'mysql':
case 'maria':
dataTypes = require('common/data-types/mysql');
break;
}
commit('ADD_CONNECTED', {
uid: connection.uid,
client: connection.client,
dataTypes,
structure: response
});
dispatch('refreshCollations', connection.uid);
dispatch('refreshVariables', connection.uid);
}
}
catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
@@ -153,11 +202,35 @@ 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 });
@@ -186,6 +259,8 @@ export default {
keyUsage: []
}],
structure: {},
variables: [],
collations: [],
breadcrumbs: {}
};
@@ -195,6 +270,11 @@ export default {
dispatch('newTab', uid);
},
changeBreadcrumbs ({ commit, getters }, payload) {
if (lastSchema !== payload.schema) {
Database.useSchema({ uid: getters.getSelected, schema: payload.schema });
lastSchema = payload.schema;
}
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: payload });
},
newTab ({ commit }, uid) {

View File

@@ -2,6 +2,7 @@ const webpack = require('webpack');
const MonacoEditorPlugin = require('monaco-editor-webpack-plugin');
module.exports = {
stats: 'errors-warnings',
plugins: [
new MonacoEditorPlugin({
languages: ['sql']