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

Compare commits

..

63 Commits

Author SHA1 Message Date
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
51ccce3da4 chore(release): 0.0.6 2020-09-03 13:46:16 +02:00
a1a6f51f2f fix: error when launching queries without a result from query tabs 2020-09-03 13:44:58 +02:00
801a0de186 fix: field name displayed instead of alias 2020-09-02 18:14:30 +02:00
264de9c568 feat: aliases support 2020-09-01 19:23:13 +02:00
8390f8aa55 Merge pull request #31 from EStarium/dependabot/npm_and_yarn/electron-10.1.0
build(deps-dev): bump electron from 9.2.1 to 10.1.0
2020-08-31 17:39:49 +02:00
af7c0e90b8 Merge pull request #30 from EStarium/dependabot/npm_and_yarn/sass-loader-10.0.1
build(deps-dev): bump sass-loader from 9.0.3 to 10.0.1
2020-08-31 14:02:57 +02:00
dependabot[bot]
da33e77361 build(deps-dev): bump electron from 9.2.1 to 10.1.0
Bumps [electron](https://github.com/electron/electron) from 9.2.1 to 10.1.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v9.2.1...v10.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-31 06:47:39 +00:00
dependabot[bot]
a4841ab63b build(deps-dev): bump sass-loader from 9.0.3 to 10.0.1
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 9.0.3 to 10.0.1.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v9.0.3...v10.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-31 06:44:49 +00:00
de3f36a3fe docs: update README.md 2020-08-21 15:56:29 +02:00
8dc74ef2c3 feat: sql suggestions in query editor 2020-08-21 11:38:00 +02:00
256ec76588 feat: middle click to close tabs 2020-08-21 10:57:26 +02:00
196a3e0185 feat: monaco-editor as query editor 2020-08-20 18:06:02 +02:00
bc54fef0aa docs: Update README.md 2020-08-20 15:31:50 +02:00
a5b478e53d Merge pull request #29 from Mohd-PH/master
Add Arabic translation
2020-08-20 15:24:03 +02:00
Mohd-PH
2e235ad2fe Change the name of Arabic in the settings page 2020-08-20 16:21:11 +03:00
Mohd-PH
950bb17b1e Add Arabic translation 2020-08-20 12:37:26 +03:00
3a6ea76b93 feat: tabs horizontal scroll with mouse wheel 2020-08-20 10:38:18 +02:00
d7ed00f4a3 feat: support to multiple query tabs 2020-08-19 18:20:57 +02:00
fd6d5177ef fix: wrong table height calc in some cases 2020-08-19 16:25:42 +02:00
9599b43f78 refactor: changed event names to kebab-case 2020-08-18 18:03:59 +02:00
2cfb223ff6 chore: Improved changelog 2020-08-17 17:48:21 +02:00
76 changed files with 8289 additions and 912 deletions

View File

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

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

@@ -2,31 +2,78 @@
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.5](https://github.com/EStarium/antares/compare/v0.0.4...v0.0.5) (2020-08-17)
### [0.0.7](https://github.com/EStarium/antares/compare/v0.0.6...v0.0.7) (2020-10-03)
### Features
* badge on setting icon and update tab when update is available ([e8141b6](https://github.com/EStarium/antares/commit/e8141b632154f765ca73fa50b9b7120dc592ead0))
* foreign key support in add/edit row ([0b6a188](https://github.com/EStarium/antares/commit/0b6a188d1959b80b4a66946cc79d2dd3853a428b))
* option to insert table rows ([2f1dfdc](https://github.com/EStarium/antares/commit/2f1dfdc6543b4a6c1d595f0daa00c0832be49c77))
* database creation ([3d0a83f](https://github.com/EStarium/antares/commit/3d0a83f2cf68c4dd412fd7679c39d63f081b7c19))
* databases deletion ([4288a1f](https://github.com/EStarium/antares/commit/4288a1fd331f4a28de2e756f898d208a6a6599c4))
* edit database collation ([54717e1](https://github.com/EStarium/antares/commit/54717e1f6a36ec0b3dd096d0e1e747512f6dda09))
* field comment on mouse over a table field name ([2554444](https://github.com/EStarium/antares/commit/2554444322b59a6b1ab3ff05ccf8604bf6f8c8b8))
* support to multiple queries in the same tab ([48f77ba](https://github.com/EStarium/antares/commit/48f77bae01efbff40bd0f5ce8c66e2619f44bf3a))
* update italian translation ([89c3dc9](https://github.com/EStarium/antares/commit/89c3dc9fede63c77eb22b48df1a375ea44830306))
* Update italian translation ([fe3d741](https://github.com/EStarium/antares/commit/fe3d7416013c44a4974471ab59b7c9a98afb7255))
### Bug Fixes
* insert files via add row option ([3c6e818](https://github.com/EStarium/antares/commit/3c6e818ba06f1b8b5db0ecf80c3b7498d6d2a841))
* newline replaced with undefined inside queries ([59e4a79](https://github.com/EStarium/antares/commit/59e4a79f42076b3fce98a764e9ad6a01c674555b))
* query result table header didn't show just selected fields ([7bc1009](https://github.com/EStarium/antares/commit/7bc10092fe4823e03133e69e0a7bf86e44fde43b))
* table header not fixed on top when fast scrolling ([13b0816](https://github.com/EStarium/antares/commit/13b0816837461119eaab79fdb7e92223e0950630))
* time and datetime precision ([771f8a2](https://github.com/EStarium/antares/commit/771f8a2d682c64105231e3fef199f05150596298))
* update a row with a string key value ([eb348b3](https://github.com/EStarium/antares/commit/eb348b3095b6905321b62eed6cea228374ebc3d1))
* window title not perfectly centered ([7651d05](https://github.com/EStarium/antares/commit/7651d05b37970574d6ae4bdf75c20c69d59c1e6d))
* wrong schema passed in query tab when selected a different database ([6d0724d](https://github.com/EStarium/antares/commit/6d0724dc90cdebb10e0342d2c472bdd07aa345f8))
* 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.4](https://github.com/EStarium/antares/compare/v0.0.3-alpha...v0.0.4) (2020-08-06)
### [0.0.6](https://github.com/EStarium/antares/compare/v0.0.5...v0.0.6) (2020-09-03)
### Features
* blob fields edit/view/download ([712fe9f](https://github.com/EStarium/antares/commit/712fe9f00d210db0f2317eca61e7fb648383e3fe))
* window title in app title bar ([0089c0c](https://github.com/EStarium/antares/commit/0089c0cbac6caf0a6fd195849099f18713580228))
* 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))
### [0.0.5](https://github.com/EStarium/antares/compare/v0.0.4...v0.0.5) (2020-08-17)
### Features
* Badge on setting icon and update tab when update is available ([e8141b6](https://github.com/EStarium/antares/commit/e8141b632154f765ca73fa50b9b7120dc592ead0))
* Foreign key support in add/edit row ([0b6a188](https://github.com/EStarium/antares/commit/0b6a188d1959b80b4a66946cc79d2dd3853a428b))
* Option to insert table rows ([2f1dfdc](https://github.com/EStarium/antares/commit/2f1dfdc6543b4a6c1d595f0daa00c0832be49c77))
### Bug Fixes
* Insert files via add row option ([3c6e818](https://github.com/EStarium/antares/commit/3c6e818ba06f1b8b5db0ecf80c3b7498d6d2a841))
* Newline replaced with undefined inside queries ([59e4a79](https://github.com/EStarium/antares/commit/59e4a79f42076b3fce98a764e9ad6a01c674555b))
* Query result header didn't show just selected fields ([7bc1009](https://github.com/EStarium/antares/commit/7bc10092fe4823e03133e69e0a7bf86e44fde43b))
* Table header not fixed on top when fast scrolling ([13b0816](https://github.com/EStarium/antares/commit/13b0816837461119eaab79fdb7e92223e0950630))
* Time and datetime precision ([771f8a2](https://github.com/EStarium/antares/commit/771f8a2d682c64105231e3fef199f05150596298))
* Update a row with a string key value ([eb348b3](https://github.com/EStarium/antares/commit/eb348b3095b6905321b62eed6cea228374ebc3d1))
* Window title not perfectly centered ([7651d05](https://github.com/EStarium/antares/commit/7651d05b37970574d6ae4bdf75c20c69d59c1e6d))
* Wrong schema passed in query tab when a different database was selected ([6d0724d](https://github.com/EStarium/antares/commit/6d0724dc90cdebb10e0342d2c472bdd07aa345f8))
### [0.0.4](https://github.com/EStarium/antares/compare/v0.0.3-alpha...v0.0.4) (2020-08-06)
### Features
* Blob fields edit/view/download ([712fe9f](https://github.com/EStarium/antares/commit/712fe9f00d210db0f2317eca61e7fb648383e3fe))
* Window title in app title bar ([0089c0c](https://github.com/EStarium/antares/commit/0089c0cbac6caf0a6fd195849099f18713580228))

View File

@@ -9,13 +9,17 @@
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 in a development state, it lacks many features, and is''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, 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. At moment i'm testing only on Windows.
🔗 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
## Philosophy
Why am I developing an SQL client when there are a lot of thom on the market?-->
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 right places, not hundreds of tiny buttons or submenu.
## How to contribute
@@ -26,8 +30,9 @@ Why am I developing an SQL client when there are a lot of thom on the market?-->
This is a roadmap with major features will come in near future.
- Improvements of query editor area.
- Multiple query tabs.
- Database management (add/edit/delete).
- Tables management (add/edit/delete).
- Users management (add/edit/delete).
- Stored procedures, views, schedulers and trigger support.
- Database tools.
- Context menu shortcuts.
@@ -36,6 +41,7 @@ This is a roadmap with major features will come in near future.
- Query logs console.
- Fake data filler.
- Import/export and migration.
- SSH tunnel.
- Themes.
## Currently supported
@@ -51,10 +57,20 @@ This is a roadmap with major features will come in near future.
### Operating Systems
#### • x86
- [x] Windows
- [x] Linux
- [x] MacOS (needs tests)
#### • ARM
- [ ] Windows
- [ ] Linux
- [ ] MacOS
## Translations
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/EStarium/antares/pull/20)
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/EStarium/antares/pull/20)
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/EStarium/antares/pull/29)
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/EStarium/antares/pull/32)

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.5",
"version": "0.0.7",
"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": {
@@ -40,54 +42,53 @@
}
},
"electronWebpack": {
"whiteListedModules": [
"codemirror"
],
"renderer": {
"webpackConfig": "webpack.config.js"
}
},
"dependencies": {
"@mdi/font": "^5.5.55",
"codemirror": "^5.56.0",
"electron-log": "^4.2.2",
"electron-updater": "^4.3.4",
"lodash": "^4.17.19",
"moment": "^2.27.0",
"mssql": "^6.2.1",
"@mdi/font": "^5.6.55",
"electron-log": "^4.2.4",
"electron-updater": "^4.3.5",
"lodash": "^4.17.20",
"moment": "^2.29.0",
"monaco-editor": "^0.20.0",
"mssql": "^6.2.2",
"mysql": "^2.18.1",
"pg": "^8.3.0",
"pg": "^8.3.3",
"source-map-support": "^0.5.16",
"spectre.css": "^0.5.9",
"vue-click-outside": "^1.1.0",
"vue-i18n": "^8.20.0",
"vue-i18n": "^8.21.0",
"vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.0",
"vuedraggable": "^2.24.1",
"vuex": "^3.5.1",
"vuex-persist": "^2.2.0"
"vuex-persist": "^3.1.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.2",
"electron": "^9.1.2",
"electron-builder": "^22.8.0",
"electron": "^10.1.0",
"electron-builder": "^22.8.1",
"electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.6.0",
"eslint": "^7.8.1",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.22.0",
"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.1",
"node-sass": "^4.14.1",
"sass-loader": "^9.0.3",
"sass-loader": "^10.0.2",
"standard-version": "^9.0.0",
"stylelint": "^13.6.1",
"stylelint": "^13.7.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"vue": "^2.6.11",
"webpack": "^4.44.1"
"vue": "^2.6.12",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.2"
}
}

View File

@@ -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,7 +1,7 @@
import { app, ipcMain } from 'electron';
export default () => {
ipcMain.on('closeApp', () => {
ipcMain.on('close-app', () => {
app.exit();
});
};

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('testConnection', async (event, conn) => {
const Connection = new AntaresConnector({
export default connections => {
ipcMain.handle('test-connection', async (event, conn) => {
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' };
}
@@ -28,12 +27,12 @@ export default (connections) => {
}
});
ipcMain.handle('checkConnection', async (event, uid) => {
ipcMain.handle('check-connection', async (event, uid) => {
return uid in 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('rawQuery', 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) => {
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, 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,39 @@
import { ipcMain } from 'electron';
import InformationSchema from '../models/InformationSchema';
import Tables from '../models/Tables';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes';
import fs from 'fs';
// TODO: remap objects based on client
export default (connections) => {
ipcMain.handle('getTableColumns', async (event, { uid, schema, table }) => {
ipcMain.handle('get-table-columns', async (event, { uid, schema, table }) => {
try {
const result = await InformationSchema.getTableColumns(connections[uid], schema, table);// TODO: uniform column properties
const { rows } = await connections[uid]
.select('*')
.schema('information_schema')
.from('COLUMNS')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` })
.orderBy({ ORDINAL_POSITION: 'ASC' })
.run();
const result = rows.map(field => {
return {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE,
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
numPrecision: field.NUMERIC_PRECISION,
datePrecision: field.DATETIME_PRECISION,
charLength: field.CHARACTER_MAXIMUM_LENGTH,
isNullable: field.IS_NULLABLE,
default: field.COLUMN_DEFAULT,
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment'),
comment: field.COLUMN_COMMENT
};
});
return { status: 'success', response: result };
}
catch (err) {
@@ -15,9 +41,15 @@ export default (connections) => {
}
});
ipcMain.handle('getTableData', async (event, { uid, schema, table }) => {
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();
return { status: 'success', response: result };
}
catch (err) {
@@ -27,7 +59,27 @@ export default (connections) => {
ipcMain.handle('get-key-usage', async (event, { uid, schema, table }) => {
try {
const result = await InformationSchema.getKeyUsage(connections[uid], schema, table);
const { rows } = await connections[uid]
.select('*')
.schema('information_schema')
.from('KEY_COLUMN_USAGE')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run();
const result = rows.map(field => {
return {
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
column: field.COLUMN_NAME,
position: field.ORDINAL_POSITION,
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
constraintName: field.CONSTRAINT_NAME,
refSchema: field.REFERENCED_TABLE_SCHEMA,
refTable: field.REFERENCED_TABLE_NAME,
refColumn: field.REFERENCED_COLUMN_NAME
};
});
return { status: 'success', response: result };
}
catch (err) {
@@ -35,9 +87,50 @@ export default (connections) => {
}
});
ipcMain.handle('updateTableCell', async (event, params) => {
ipcMain.handle('update-table-cell', async (event, params) => {
try {
const result = await Tables.updateTableCell(connections[params.uid], 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 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() };
}
});
ipcMain.handle('delete-table-rows', async (event, params) => {
try {
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) {
@@ -45,19 +138,41 @@ export default (connections) => {
}
});
ipcMain.handle('deleteTableRows', async (event, params) => {
ipcMain.handle('insert-table-rows', async (event, params) => {
try {
const result = await Tables.deleteTableRows(connections[params.uid], params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
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();
}
ipcMain.handle('insertTableRows', async (event, params) => {
try {
await Tables.insertTableRows(connections[params.uid], params);
return { status: 'success' };
}
catch (err) {
@@ -67,7 +182,17 @@ export default (connections) => {
ipcMain.handle('get-foreign-list', async (event, params) => {
try {
const results = await Tables.getForeignList(connections[params.uid], params);
const query = connections[params.uid]
.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`);
const results = await query.run();
return { status: 'success', response: results };
}
catch (err) {

View File

@@ -2,39 +2,40 @@ import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
let mainWindow;
autoUpdater.allowPrerelease = true;
export default () => {
ipcMain.on('checkForUpdates', event => {
ipcMain.on('check-for-updates', event => {
mainWindow = event;
autoUpdater.checkForUpdatesAndNotify().catch(() => {
mainWindow.reply('checkFailed');
mainWindow.reply('check-failed');
});
});
ipcMain.on('restartToUpdate', () => {
ipcMain.on('restart-to-update', () => {
autoUpdater.quitAndInstall();
});
// auto-updater events
autoUpdater.on('checking-for-update', () => {
mainWindow.reply('checkingForUpdate');
mainWindow.reply('checking-for-update');
});
autoUpdater.on('update-available', () => {
mainWindow.reply('updateAvailable');
mainWindow.reply('update-available');
});
autoUpdater.on('update-not-available', () => {
mainWindow.reply('updateNotAvailable');
mainWindow.reply('update-not-available');
});
autoUpdater.on('download-progress', (data) => {
mainWindow.reply('downloadProgress', data);
autoUpdater.on('download-progress', data => {
mainWindow.reply('download-progress', data);
});
autoUpdater.on('update-downloaded', () => {
mainWindow.reply('updateDownloaded');
mainWindow.reply('update-downloaded');
});
autoUpdater.logger = require('electron-log');

View File

@@ -1,323 +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, fields } = await new Promise((resolve, reject) => {
this._connection.query(sql, (err, rows, fields) => {
if (err)
reject(err);
else
resolve({ rows, fields });
});
});
return { rows, 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,141 @@
'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;
}
/**
* @returns {Promise}
* @memberof AntaresCore
*/
async run () {
const rawQuery = this.getSQL();
this._resetQuery();
return this.raw(rawQuery);
}
}

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,210 @@
'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 => { // TODO: remap all objects,
return {
name: db.Database,
tables: tables.filter(table => table.TABLE_SCHEMA === db.Database),
functions: functions.filter(func => func.Db === db.Database),
procedures: procedures.filter(procedure => procedure.Db === db.Database),
triggers: triggersArr.filter(trigger => trigger.Db === db.Database),
schedulers: schedulers.filter(scheduler => scheduler.Db === db.Database)
};
});
}
/**
* 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
};
});
}
/**
* @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 {boolean} [nest]
* @returns {Promise}
* @memberof MySQLClient
*/
async raw (sql, nest) {
const nestTables = nest ? '.' : false;
const resultsArr = [];
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;
const { rows, report, fields } = await new Promise((resolve, reject) => {
this._connection.query({ sql: query, nestTables }, (err, response, fields) => {
if (err)
reject(err);
else {
resolve({
rows: Array.isArray(response) ? response : false,
report: !Array.isArray(response) ? response : false,
fields
});
}
});
});
resultsArr.push({ rows, report, fields });
}
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

@@ -4,7 +4,7 @@
<div id="window-content">
<TheSettingBar />
<div id="main-content" class="container">
<TheAppWelcome v-if="!connections.length" @newConn="showNewConnModal" />
<TheAppWelcome v-if="!connections.length" @new-conn="showNewConnModal" />
<div v-else class="columns col-gapless">
<Workspace
v-for="connection in connections"
@@ -52,7 +52,7 @@ export default {
})
},
mounted () {
ipcRenderer.send('checkForUpdates');
ipcRenderer.send('check-for-updates');
},
methods: {
...mapActions({

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>

View File

@@ -46,8 +46,8 @@ export default {
justify-content: center;
align-items: center;
overflow: hidden;
padding: 0.4rem;
position: fixed;
height: 100vh;
right: 0;
top: 0;
left: 0;
@@ -72,6 +72,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">
@@ -65,7 +65,7 @@ export default {
},
methods: {
closeModal () {
this.$emit('closeAsking');
this.$emit('close-asking');
},
sendCredentials () {
this.$emit('credentials', this.credentials);

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}"
@@ -137,7 +137,7 @@
</div>
<ModalAskCredentials
v-if="isAsking"
@closeAsking="closeAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>

View File

@@ -0,0 +1,151 @@
<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
};
},
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');
}
}
};
</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}"
@@ -141,7 +141,7 @@
</div>
<ModalAskCredentials
v-if="isAsking"
@closeAsking="closeAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>

View File

@@ -0,0 +1,125 @@
<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 };
},
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');
}
}
};
</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">
@@ -148,10 +148,10 @@ export default {
return this.keyUsage.map(key => key.column);
},
fields () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields : [];
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields[0] : [];
},
keyUsage () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage : [];
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage[0] : [];
}
},
watch: {

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)"
@@ -68,10 +71,10 @@ export default {
},
methods: {
checkForUpdates () {
ipcRenderer.send('checkForUpdates');
ipcRenderer.send('check-for-updates');
},
restartToUpdate () {
ipcRenderer.send('restartToUpdate');
ipcRenderer.send('restart-to-update');
}
}
};

View File

@@ -1,25 +1,15 @@
<template>
<div class="editor-wrapper">
<textarea
ref="codemirror"
:options="cmOptions"
/>
<div ref="editor" class="editor" />
</div>
</template>
<script>
import CodeMirror from 'codemirror';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material-darker.css';
import 'codemirror/mode/sql/sql';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/sql-hint';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { completionItemProvider } from '@/suggestions/sql';
CodeMirror.defineOption('sql-hint');
monaco.languages.registerCompletionItemProvider('sql', completionItemProvider(monaco));
export default {
name: 'QueryEditor',
@@ -28,42 +18,31 @@ export default {
},
data () {
return {
cminstance: null,
content: '',
cmOptions: {
tabSize: 3,
smartIndent: true,
styleActiveLine: true,
lineNumbers: true,
line: true,
mode: 'text/x-sql',
theme: 'material-darker',
extraKeys: {
'Ctrl-Space': 'autocomplete'
},
hintOptions: {
tables: {
users: ['name', 'score', 'birthDate'],
countries: ['name', 'population', 'size']
}
},
autoCloseBrackets: true
}
editor: null
};
},
mounted () {
this.initialize();
},
methods: {
initialize () {
this.cminstance = CodeMirror.fromTextArea(this.$refs.codemirror, this.cmOptions);
this.cminstance.setValue(this.value || this.content);
this.editor = monaco.editor.create(this.$refs.editor, {
value: this.value,
language: 'sql',
theme: 'vs-dark',
autoIndent: true,
minimap: {
enabled: false
},
contextmenu: false,
wordBasedSuggestions: true,
acceptSuggestionOnEnter: 'smart',
quickSuggestions: true
});
this.cminstance.on('change', cm => {
this.content = cm.getValue();
this.$emit('input', this.content);
});
}
this.editor.onDidChangeModelContent(e => {
const content = this.editor.getValue();
this.$emit('update:value', content);
});
},
beforeDestroy () {
this.editor && this.editor.dispose();
}
};
</script>
@@ -71,11 +50,14 @@ export default {
<style lang="scss">
.editor-wrapper {
border-bottom: 1px solid #444;
.editor {
height: 200px;
width: 100%;
}
}
.CodeMirror {
height: 200px;
.CodeMirror-scroll {
max-width: 100%;
}

View File

@@ -4,10 +4,10 @@
@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>
<ConfirmModal
@@ -22,7 +22,7 @@
</template>
<div :slot="'body'">
<div class="mb-2">
{{ $t('message.deleteConnectionCorfirm') }} <b>{{ connectionName }}</b>?
{{ $t('message.deleteCorfirm') }} <b>{{ connectionName }}</b>?
</div>
</div>
</ConfirmModal>

View File

@@ -11,7 +11,7 @@
{{ $t('message.appFirstStep') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="$emit('newConn')">
<button class="btn btn-primary" @click="$emit('new-conn')">
{{ $t('message.createConnection') }}
</button>
</div>

View File

@@ -11,7 +11,7 @@
<div class="footer-right-elements">
<ul class="footer-elements">
<li class="footer-element footer-link" @click="openOutside('https://www.patreon.com/fabio286')">
<li class="footer-element footer-link" @click="openOutside('https://github.com/sponsors/Fabio286')">
<i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small>
</li>

View File

@@ -201,6 +201,7 @@ export default {
max-width: 320px;
pointer-events: none;
text-overflow: ellipsis;
overflow: hidden;
transition: opacity 0.2s;
}

View File

@@ -73,7 +73,7 @@ export default {
},
methods: {
closeApp () {
ipcRenderer.send('closeApp');
ipcRenderer.send('close-app');
},
minimizeApp () {
this.w.minimize();

View File

@@ -2,21 +2,23 @@
<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 class="tab tab-block column col-12">
<!-- <li
<ul ref="tabWrap" class="tab tab-block column col-12">
<li
v-if="workspace.breadcrumbs.table"
class="tab-item"
:class="{'active': selectedTab === 'prop'}"
@click="selectTab({uid: workspace.uid, tab: 'prop'})"
>
<a class="tab-link">
<i class="mdi mdi-18px mdi-tune mr-1" />
<span :title="workspace.breadcrumbs.table">{{ $t('word.properties').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span>
</a>
</li> -->
</li>
<li
v-if="workspace.breadcrumbs.table"
class="tab-item"
:class="{'active': selectedTab === 1}"
@click="selectTab({uid: workspace.uid, tab: 1})"
:class="{'active': selectedTab === 'data'}"
@click="selectTab({uid: workspace.uid, tab: 'data'})"
>
<a class="tab-link">
<i class="mdi mdi-18px mdi-table mr-1" />
@@ -24,25 +26,50 @@
</a>
</li>
<li
v-for="(tab, key) of queryTabs"
v-for="tab of queryTabs"
:key="tab.uid"
class="tab-item"
:class="{'active': selectedTab === tab.uid}"
@click="selectTab({uid: workspace.uid, tab: tab.uid})"
@mousedown.middle="closeTab(tab.uid)"
>
<a><span>Query #{{ key+1 }} <span v-if="queryTabs.length > 1" class="btn btn-clear" /></span></a>
<a>
<span>
Query #{{ tab.index }}
<span
v-if="queryTabs.length > 1"
class="btn btn-clear"
:title="$t('word.close')"
@click.stop="closeTab(tab.uid)"
/>
</span>
</a>
</li>
<li class="tab-item">
<a
class="tab-add"
:title="$t('message.openNewTab')"
@click="addTab"
>
<i class="mdi mdi-24px mdi-plus" />
</a>
</li>
</ul>
<div v-show="selectedTab === 'prop'" class="column col-12">
<p class="px-2">
In future releases
</p>
</div>
<WorkspaceTableTab
v-show="selectedTab === 1"
v-show="selectedTab === 'data'"
:connection="connection"
:table="workspace.breadcrumbs.table"
/>
<WorkspaceQueryTab
v-for="tab of queryTabs"
v-show="selectedTab === tab.uid"
:key="tab.uid"
:tab-uid="tab.uid"
:is-selected="selectedTab === tab.uid"
:connection="connection"
/>
</div>
@@ -78,7 +105,7 @@ export default {
return this.selectedWorkspace === this.connection.uid;
},
selectedTab () {
return this.workspace.selected_tab || this.queryTabs[0].uid;
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) || ['data', 'prop'].includes(this.workspace.selected_tab) ? this.workspace.selected_tab : this.queryTabs[0].uid;
},
queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query');
@@ -90,13 +117,30 @@ export default {
if (isInitiated)
this.connectWorkspace(this.connection);
},
mounted () {
if (this.$refs.tabWrap) {
this.$refs.tabWrap.addEventListener('wheel', e => {
if (e.deltaY > 0) this.$refs.tabWrap.scrollLeft += 50;
else this.$refs.tabWrap.scrollLeft -= 50;
});
}
},
methods: {
...mapActions({
addWorkspace: 'workspaces/addWorkspace',
connectWorkspace: 'workspaces/connectWorkspace',
removeConnected: 'workspaces/removeConnected',
selectTab: 'workspaces/selectTab'
})
selectTab: 'workspaces/selectTab',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab'
}),
addTab () {
this.newTab(this.connection.uid);
},
closeTab (tUid) {
if (this.queryTabs.length === 1) return;
this.removeTab({ uid: this.connection.uid, tab: tUid });
}
}
};
</script>
@@ -107,12 +151,21 @@ export default {
margin: 0;
.workspace-tabs {
overflow: auto;
overflow: hidden;
height: calc(100vh - #{$excluding-size});
.tab-block {
background: $bg-color-light;
margin-top: 0;
flex-direction: row;
align-items: flex-start;
flex-wrap: nowrap;
overflow: auto;
&::-webkit-scrollbar {
width: 2px;
height: 2px;
}
.tab-item {
max-width: 12rem;
@@ -132,10 +185,17 @@ export default {
opacity: 1;
}
&.tab-add {
padding: 0.2rem 0.4rem;
margin-top: 2px;
border: 0;
}
> span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 0 0.2rem;
}
}

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
@@ -19,7 +19,7 @@
</div>
<ModalAskCredentials
v-if="isAsking"
@closeAsking="closeAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>

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

@@ -4,6 +4,7 @@
class="accordion-header database-name pb-0"
:class="{'text-bold': breadcrumbs.schema === database.name}"
@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" />
@@ -18,6 +19,7 @@
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.TABLE_NAME}"
@click="changeBreadcrumbs({schema: database.name, table: table.TABLE_NAME})"
@contextmenu.prevent="showTableContext($event, table.TABLE_NAME)"
>
<a class="table-name">
<i class="table-icon mdi mdi-18px mdi-table mr-1" />
@@ -50,7 +52,13 @@ export default {
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
})
}),
showDatabaseContext (event, database) {
this.$emit('show-database-context', { event, database });
},
showTableContext (table) {
this.$emit('show-table-context', table);
}
}
};
</script>

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

@@ -1,7 +1,7 @@
<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">
<QueryEditor v-model="query" />
<QueryEditor v-if="isSelected" :value.sync="query" />
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
@@ -15,8 +15,11 @@
</button>
</div>
<div class="workspace-query-info">
<div v-if="results.rows">
{{ $t('word.results') }}: <b>{{ results.rows.length }}</b>
<div v-if="resultsCount !== false">
{{ $t('word.results') }}: <b>{{ resultsCount }}</b>
</div>
<div v-if="affectedCount !== false">
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b>
</div>
<div v-if="workspace.breadcrumbs.schema">
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.schema }}</b>
@@ -31,15 +34,17 @@
ref="queryTable"
:results="results"
:tab-uid="tabUid"
@updateField="updateField"
@deleteSelected="deleteSelected"
:conn-uid="connection.uid"
mode="query"
@update-field="updateField"
@delete-selected="deleteSelected"
/>
</div>
</div>
</template>
<script>
import Connection from '@/ipc-api/Connection';
import Database from '@/ipc-api/Database';
import Tables from '@/ipc-api/Tables';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
@@ -55,15 +60,17 @@ export default {
mixins: [tableTabs],
props: {
connection: Object,
tabUid: String
tabUid: String,
isSelected: Boolean
},
data () {
return {
query: '',
lastQuery: '',
isQuering: false,
results: {},
selectedFields: []
results: [],
resultsCount: false,
affectedCount: false
};
},
computed: {
@@ -72,16 +79,6 @@ 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;
}
},
methods: {
@@ -90,11 +87,25 @@ export default {
setTabFields: 'workspaces/setTabFields',
setTabKeyUsage: 'workspaces/setTabKeyUsage'
}),
getResultParams (index) {
const resultsWithRows = this.results.filter(result => result.rows);
let cachedTable;
if (resultsWithRows[index] && resultsWithRows[index].fields && resultsWithRows[index].fields.length) {
return resultsWithRows[index].fields.map(field => {
if (field.orgTable) cachedTable = field.orgTable;// Needed for some queries on information_schema
return {
table: field.orgTable || cachedTable,
schema: field.db || 'INFORMATION_SCHEMA'
};
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
}
return [];
},
async runQuery (query) {
if (!query) return;
this.isQuering = true;
this.results = {};
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
this.clearTabData();
try { // Query Data
const params = {
@@ -103,10 +114,90 @@ export default {
query
};
const { status, response } = await Connection.rawQuery(params);
const { status, response } = await Database.rawQuery(params);
if (status === 'success') {
this.results = response;
this.selectedFields = response.fields.map(field => field.orgName);
this.results = Array.isArray(response) ? response : [response];
let selectedFields = [];
const fieldsArr = [];
const keysArr = [];
let qI = 0;// queries index
for (const result of this.results) { // cycle queries
let fI = 0;// fields index
if (result.rows) { // if is a select
const paramsArr = this.getResultParams(qI);
selectedFields = result.fields.map(field => field.orgName);
this.resultsCount += result.rows.length;
for (const paramObj of paramsArr) {
try { // Table data
const params = {
uid: this.connection.uid,
...paramObj
};
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
let fields = response.filter(field => selectedFields.includes(field.name));
if (selectedFields.length) {
fields = fields.map(field => {
return { ...field, alias: result.fields[fI++].name };
});
}
if (!fields.length) {
fields = response.map(field => {
return { ...field, alias: result.fields[fI++].name };
});
}
fieldsArr[qI] = fieldsArr[qI] ? [...fieldsArr[qI], ...fields] : fields;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
try { // Key usage (foreign keys)
const params = {
uid: this.connection.uid,
...paramObj
};
const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success')
keysArr[qI] = keysArr[qI] ? [...keysArr[qI], ...response] : response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
}
else if (result.report) { // if is a query without output
this.affectedCount += result.report.affectedRows;
}
qI++;
}
this.setTabFields({
cUid: this.connection.uid,
tUid: this.tabUid,
fields: fieldsArr
});
this.setTabKeyUsage({
cUid: this.connection.uid,
tUid: this.tabUid,
keyUsage: keysArr
});
}
else
this.addNotification({ status: 'error', message: response });
@@ -115,47 +206,17 @@ export default {
this.addNotification({ status: 'error', message: err.stack });
}
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') {
const fields = response.filter(field => this.selectedFields.includes(field.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 });
}
this.isQuering = false;
this.lastQuery = query;
},
reloadTable () {
this.runQuery(this.lastQuery);
},
clearTabData () {
this.results = [];
this.resultsCount = false;
this.affectedCount = false;
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
}
}
};

View File

@@ -8,16 +8,28 @@
v-if="isContext"
:context-event="contextEvent"
:selected-rows="selectedRows"
@deleteSelected="deleteSelected"
@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].orgTable : '' }} ({{ 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.comment ? field.comment : false"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field.name)">
@@ -27,9 +39,9 @@
:class="`key-${field.key}`"
:title="keyName(field.key)"
/>
<span>{{ field.name }}</span>
<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"
@@ -56,8 +68,8 @@
:key-usage="keyUsage"
class="tr"
:class="{'selected': selectedRows.includes(row._id)}"
@selectRow="selectRow($event, row._id)"
@updateField="updateField($event, row[primaryField.name])"
@select-row="selectRow($event, row._id)"
@update-field="updateField($event, getPrimaryValue(row))"
@contextmenu="contextMenu"
/>
</template>
@@ -81,8 +93,10 @@ export default {
TableContext
},
props: {
results: Object,
tabUid: [String, Number]
results: Array,
tabUid: [String, Number],
connUid: String,
mode: String
},
data () {
return {
@@ -93,13 +107,19 @@ export default {
selectedCell: null,
selectedRows: [],
currentSort: '',
currentSortDir: 'asc'
currentSortDir: 'asc',
resultsetIndex: 0,
scrollElement: null
};
},
computed: {
...mapGetters({
getWorkspaceTab: 'workspaces/getWorkspaceTab'
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,34 @@ export default {
else
return this.localResults;
},
resultsWithRows () {
return this.results.filter(result => result.rows);
},
tabProperties () {
return this.getWorkspaceTab(this.tabUid);
},
fields () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields : [];
return this.tabProperties && this.tabProperties.fields[this.resultsetIndex] ? this.tabProperties.fields[this.resultsetIndex] : [];
},
keyUsage () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage : [];
},
scrollElement () {
return this.$refs.tableWrapper;
return this.tabProperties && this.tabProperties.keyUsage[this.resultsetIndex] ? this.tabProperties.keyUsage[this.resultsetIndex] : [];
}
},
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);
@@ -178,9 +205,34 @@ export default {
return 'UNKNOWN ' + key;
}
},
getTable (index) {
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length)
return this.resultsWithRows[index].fields[0].orgTable;
return '';
},
getSchema (index) {
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length)
return this.resultsWithRows[index].fields[0].db;
return this.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}`
].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.table;
const el = this.$refs.tableWrapper;
if (el) {
const footer = document.getElementById('footer');
@@ -199,10 +251,12 @@ export default {
else {
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id,
...payload
};
this.$emit('updateField', params);
this.$emit('update-field', params);
}
},
deleteSelected () {
@@ -212,16 +266,21 @@ export default {
const rowIDs = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => row[this.primaryField.name]);
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
rows: rowIDs
};
this.$emit('deleteSelected', params);
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 +320,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 +337,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 +370,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
@@ -63,7 +63,7 @@ export default {
this.$emit('close-context');
},
deleteRows () {
this.$emit('deleteSelected');
this.$emit('delete-selected');
this.closeContext();
}
}

View File

@@ -7,15 +7,14 @@
class="td p-0"
tabindex="0"
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})"
@updateField="updateField($event, row[primaryField.name])"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${fieldType(cKey)}`"
:class="`${isNull(col)} type-${getFieldType(cKey)}`"
@dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(fieldType(cKey), fieldPrecision(cKey)) | cutText }}</span>
>{{ col | typeFormat(getFieldType(cKey), getFieldPrecision(cKey)) | cutText }}</span>
<ForeignKeySelect
v-else-if="foreignKeys.includes(cKey)"
class="editable-field"
@@ -187,7 +186,10 @@ export default {
},
props: {
row: Object,
fields: Array,
fields: {
type: Array,
default: () => []
},
keyUsage: Array
},
data () {
@@ -218,7 +220,7 @@ export default {
if (TIME.includes(this.editingType)) {
let timeMask = '##:##:##';
const precision = this.fieldPrecision(this.editingField);
const precision = this.getFieldPrecision(this.editingField);
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
@@ -231,7 +233,7 @@ export default {
if (DATETIME.includes(this.editingType)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.fieldPrecision(this.editingField);
const precision = this.getFieldPrecision(this.editingField);
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
@@ -254,28 +256,39 @@ export default {
return this.keyUsage.map(key => key.column);
}
},
created () {
this.fields.forEach(field => {
this.isInlineEditor[field.name] = false;
});
watch: {
fields () {
this.fields.forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
},
methods: {
fieldType (cKey) {
getFieldType (cKey) {
let type = 'unknown';
const field = this.fields.filter(field => field.name === cKey)[0];
const field = this.getFieldObj(cKey);
if (field)
type = field.type;
return type;
},
fieldPrecision (cKey) {
getFieldPrecision (cKey) {
let length = 0;
const field = this.fields.filter(field => field.name === cKey)[0];
const field = this.getFieldObj(cKey);
if (field)
length = field.datePrecision;
return length;
},
getFieldObj (cKey) {
return this.fields.filter(field =>
field.name === cKey ||
field.alias === cKey ||
`${field.table}.${field.name}` === cKey ||
`${field.table}.${field.alias}` === cKey ||
`${field.table.toLowerCase()}.${field.name}` === cKey ||
`${field.table.toLowerCase()}.${field.alias}` === cKey)[0];
},
isNull (value) {
return value === null ? ' is-null' : '';
},
@@ -283,7 +296,7 @@ export default {
return bufferToBase64(val);
},
editON (event, content, field) {
const type = this.fieldType(field);
const type = this.getFieldType(field);
this.originalContent = content;
this.editingType = type;
this.editingField = field;
@@ -316,7 +329,7 @@ export default {
}
// Inline editable fields
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.fieldPrecision(field));
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.getFieldPrecision(field));
this.$nextTick(() => { // Focus on input
event.target.blur();
@@ -346,8 +359,8 @@ export default {
}
}
this.$emit('updateField', {
field: this.editingField,
this.$emit('update-field', {
field: this.getFieldObj(this.editingField).name,
type: this.editingType,
content
});
@@ -385,14 +398,11 @@ export default {
};
this.willBeDeleted = true;
},
updateField (event, id) {
this.$emit('updateField', event, id);
},
contextMenu (event, cell) {
this.$emit('updateField', event, cell);
this.$emit('update-field', event, cell);
},
selectRow (event, row) {
this.$emit('selectRow', event, row);
this.$emit('select-row', event, row);
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName);

View File

@@ -21,8 +21,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>
@@ -33,11 +33,14 @@
<div class="workspace-query-results column col-12">
<WorkspaceQueryTable
v-if="results"
v-show="!isQuering"
ref="queryTable"
:results="results"
:tab-uid="tabUid"
@updateField="updateField"
@deleteSelected="deleteSelected"
:conn-uid="connection.uid"
mode="table"
@update-field="updateField"
@delete-selected="deleteSelected"
/>
</div>
<ModalNewTableRow
@@ -69,9 +72,9 @@ export default {
},
data () {
return {
tabUid: 1,
tabUid: 'data',
isQuering: false,
results: {},
results: [],
fields: [],
keyUsage: [],
lastTable: null,
@@ -86,7 +89,7 @@ export default {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 1;
return this.workspace.selected_tab === 'data';
}
},
watch: {
@@ -115,12 +118,14 @@ export default {
async getTableData () {
if (!this.table) return;
this.isQuering = true;
this.results = {};
this.results = [];
const fieldsArr = [];
const keysArr = [];
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
const params = {
uid: this.connection.uid,
schema: this.workspace.breadcrumbs.schema,
schema: this.schema,
table: this.workspace.breadcrumbs.table
};
@@ -128,7 +133,7 @@ export default {
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 });
fieldsArr.push(response);
}
else
this.addNotification({ status: 'error', message: response });
@@ -141,7 +146,7 @@ export default {
const { status, response } = await Tables.getTableData(params);
if (status === 'success')
this.results = response;
this.results = [response];
else
this.addNotification({ status: 'error', message: response });
}
@@ -151,9 +156,10 @@ export default {
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 });
keysArr.push(response);
}
else
this.addNotification({ status: 'error', message: response });
@@ -162,8 +168,13 @@ export default {
this.addNotification({ status: 'error', message: err.stack });
}
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: fieldsArr });
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: keysArr });
this.isQuering = false;
},
getTable () {
return this.table;
},
reloadTable () {
this.getTableData();
},

View File

@@ -0,0 +1,87 @@
module.exports = {
word: {
edit: 'تعديل',
save: 'حفظ',
close: 'إغلاق',
delete: 'حفظ',
confirm: 'تأكيد',
cancel: 'إلغاء',
send: 'إرسال',
connectionName: 'إسم الإتصال',
client: 'العميل',
hostName: 'إسم المستضيف',
port: 'المنفذ',
user: 'المستخدم',
password: 'الرقم السري',
credentials: 'بيانات الدخول',
connect: 'إتصال',
connected: 'متصل',
disconnect: 'إلغاء الإتصال',
disconnected: 'غير متصل',
refresh: 'تحديث',
settings: 'الإعدادات',
general: 'عام',
themes: 'الأنماط',
update: 'تحديث',
about: 'حول',
language: 'اللغة',
version: 'النسخة',
donate: 'إدعم',
run: 'شغل',
schema: 'Schema',
results: 'النتائج',
size: 'الحجم',
seconds: 'ثواني',
type: 'نوع',
mimeType: 'نوع الميديا',
download: 'تحميل',
add: 'أضف',
data: 'بيانات',
properties: 'خصائص',
insert: 'أدرج'
},
message: {
appWelcome: 'مرحبا بك في عميل الSQL انتاريس!',
appFirstStep: 'خطوتك الأولى قم بإنشاء إتصال جديد بقاعدة بيانات.',
addConnection: 'إضافة إتصال',
createConnection: 'إنشاء إتصال',
createNewConnection: 'إنشاء إتصال جديد',
askCredentials: 'إطلب بيانات الدخول',
testConnection: 'إختبر الإتصال',
editConnection: 'عدل الإتصال',
deleteConnection: 'إحذف الإتصال',
deleteCorfirm: 'هل أنت متأكد من حذف الإتصال؟',
connectionSuccessfullyMade: 'تم الإتصال بنجاح!',
madeWithJS: 'بني بـ 💛 و جافاسكربت!',
checkForUpdates: 'تأكد من التحديثات',
noUpdatesAvailable: 'لا توجد تحديثات',
checkingForUpdate: 'البحث عن تحديثات',
checkFailure: 'فشل البحث, نرجوا المحاولة في وقت لاحق',
updateAvailable: 'تحديث جديد متوفر',
downloadingUpdate: 'جاري تحميل التحديث',
updateDownloaded: 'تم تحميل التحديث',
restartToInstall: 'قم بإعادة تشغيل انتاريس للتحديث',
unableEditFieldWithoutPrimary: 'لا يمكن تعديل الخانة بدون وجود مفتاح رئيسي في النتائج',
editCell: 'تعديل الخلية',
deleteRows: 'حذف صف | حذف {count} صفوف',
confirmToDeleteRows: 'هل أنت متأكد من حذف صف واحد؟? | هل أنت متأكد من حذف {count} صف?',
notificationsTimeout: 'إنتهاء التنبيهات',
uploadFile: 'رفع ملف',
addNewRow: 'إضافة صف جديد',
numberOfInserts: 'عدد الإدراجات'
},
// 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

@@ -38,7 +38,10 @@ module.exports = {
add: 'Add',
data: 'Data',
properties: 'Properties',
insert: 'Insert'
insert: 'Insert',
connecting: 'Connecting',
name: 'Name',
collation: 'Collation'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -50,7 +53,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',
@@ -68,7 +71,14 @@ module.exports = {
notificationsTimeout: 'Notifications timeout',
uploadFile: 'Upload file',
addNewRow: 'Add new row',
numberOfInserts: 'Number of inserts'
numberOfInserts: 'Number of inserts',
openNewTab: 'Open a new tab',
affectedRows: 'Affected rows',
createNewDatabase: 'Create new Database',
databaseName: 'Database name',
serverDefault: 'Server default',
deleteDatabase: 'Delete database',
editDatabase: 'Edit database'
},
// 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

@@ -6,7 +6,9 @@ Vue.use(VueI18n);
const i18n = new VueI18n({
messages: {
'en-US': require('./en-US'),
'it-IT': require('./it-IT')
'it-IT': require('./it-IT'),
'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,4 +1,6 @@
export default {
'en-US': 'English',
'it-IT': 'Italiano'
'it-IT': 'Italiano',
'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

@@ -3,11 +3,11 @@ import { ipcRenderer } from 'electron';
export default class {
static makeTest (params) {
return ipcRenderer.invoke('testConnection', params);
return ipcRenderer.invoke('test-connection', params);
}
static checkConnection (params) {
return ipcRenderer.invoke('checkConnection', params);
return ipcRenderer.invoke('check-connection', params);
}
static connect (params) {
@@ -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('rawQuery', 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

@@ -3,11 +3,11 @@ import { ipcRenderer } from 'electron';
export default class {
static getTableColumns (params) {
return ipcRenderer.invoke('getTableColumns', params);
return ipcRenderer.invoke('get-table-columns', params);
}
static getTableData (params) {
return ipcRenderer.invoke('getTableData', params);
return ipcRenderer.invoke('get-table-data', params);
}
static getKeyUsage (params) {
@@ -15,15 +15,15 @@ export default class {
}
static updateTableCell (params) {
return ipcRenderer.invoke('updateTableCell', params);
return ipcRenderer.invoke('update-table-cell', params);
}
static deleteTableRows (params) {
return ipcRenderer.invoke('deleteTableRows', params);
return ipcRenderer.invoke('delete-table-rows', params);
}
static insertTableRows (params) {
return ipcRenderer.invoke('insertTableRows', params);
return ipcRenderer.invoke('insert-table-rows', params);
}
static getForeignList (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

@@ -33,6 +33,8 @@
"blob": darkorchid,
"mediumblob": darkorchid,
"longblob": darkorchid,
"enum": gold,
"set": gold,
"unknown": gray,
)
);

View File

@@ -106,6 +106,13 @@ body {
.tab {
border-color: #272727;
.tab-item {
.btn-clear {
margin-top: -0.1rem;
font-size: 0.6rem;
}
}
}
.panel {
@@ -126,12 +133,12 @@ body {
}
.form-select,
.form-select:not([multiple]):not([size]),
.form-input,
.form-select:not([multiple]):not([size]),
.form-checkbox .form-icon,
.form-radio .form-icon {
border-color: $bg-color-light;
background: $bg-color-gray;
background-color: $bg-color-gray;
}
.form-input:not(:placeholder-shown):invalid:focus {

View File

@@ -3,7 +3,7 @@ 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,

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,18 +1,9 @@
'use strict';
import Connection from '@/ipc-api/Connection';
import Database from '@/ipc-api/Database';
import { uidGen } from 'common/libs/uidGen';
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)
};
});
}
const tabIndex = [];
let lastSchema = '';
export default {
namespaced: true,
@@ -30,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);
@@ -56,6 +50,12 @@ export default {
REFRESH_STRUCTURE (state, { uid, structure }) {
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);
},
@@ -63,8 +63,11 @@ export default {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, breadcrumbs } : workspace);
},
NEW_TAB (state, uid) {
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
const newTab = {
uid: uidGen('T'),
index: tabIndex[uid],
selected: false,
type: 'query',
fields: [],
@@ -81,6 +84,18 @@ export default {
return workspace;
});
},
REMOVE_TAB (state, { uid, tab: tUid }) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
return {
...workspace,
tabs: workspace.tabs.filter(tab => tab.uid !== tUid)
};
}
else
return workspace;
});
},
SELECT_TAB (state, { uid, tab }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selected_tab: tab } : workspace);
},
@@ -120,7 +135,7 @@ export default {
}
},
actions: {
selectWorkspace ({ commit }, uid) {
selectWorkspace ({ commit, dispatch }, uid) {
commit('SELECT_WORKSPACE', uid);
},
async connectWorkspace ({ dispatch, commit }, connection) {
@@ -128,8 +143,11 @@ 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 {
commit('ADD_CONNECTED', { uid: connection.uid, structure: response });
dispatch('refreshCollations', connection.uid);
dispatch('refreshVariables', connection.uid);
}
}
catch (err) {
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
@@ -137,11 +155,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 });
@@ -158,26 +200,42 @@ export default {
connected: false,
selected_tab: 0,
tabs: [{
uid: 1,
uid: 'data',
type: 'table',
fields: [],
keyUsage: []
},
{
uid: 'prop',
type: 'table',
fields: [],
keyUsage: []
}],
structure: {},
variables: [],
collations: [],
breadcrumbs: {}
};
commit('ADD_WORKSPACE', workspace);
if (getters.getWorkspace(uid).tabs.length < 2)
if (getters.getWorkspace(uid).tabs.length < 3)
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) {
commit('NEW_TAB', uid);
},
removeTab ({ commit }, payload) {
commit('REMOVE_TAB', payload);
},
selectTab ({ commit }, payload) {
commit('SELECT_TAB', payload);
},

View File

@@ -1,23 +1,23 @@
import { ipcRenderer } from 'electron';
export default store => {
ipcRenderer.on('checkingForUpdate', () => {
ipcRenderer.on('checking-for-update', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'checking');
});
ipcRenderer.on('updateAvailable', () => {
ipcRenderer.on('update-available', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'available');
});
ipcRenderer.on('updateNotAvailable', () => {
ipcRenderer.on('update-not-available', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'noupdate');
});
ipcRenderer.on('checkFailed', () => {
ipcRenderer.on('check-failed', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'nocheck');
});
ipcRenderer.on('downloadProgress', (event, data) => {
ipcRenderer.on('download-progress', (event, data) => {
store.commit('application/CHANGE_UPDATE_STATUS', 'downloading');
store.commit('application/CHANGE_PROGRESS_PERCENTAGE', data.percent);
});
ipcRenderer.on('updateDownloaded', () => {
ipcRenderer.on('update-downloaded', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'downloaded');
});
};

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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