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

Compare commits

...

74 Commits

Author SHA1 Message Date
c2f76e490a chore(release): 0.1.1 2021-04-03 12:21:34 +02:00
e349dd5eab feat: scratchpad to save persistent notes 2021-04-03 12:17:40 +02:00
280697698e feat(UI): light theme 2021-04-03 11:21:58 +02:00
0783f8b57e chore: update README.md 2021-04-02 15:27:29 +02:00
c981244d7a fix(UI): editor theme preview not properly loaded in some cases 2021-04-01 15:26:49 +02:00
dcb135dd01 fix: hide update tab for Windows Store distributions 2021-04-01 14:12:49 +02:00
99f7511c4d feat(PostgreSQL): views management 2021-03-31 16:54:06 +02:00
fe4c8e12b3 feat(PostgreSQL): foreign keys management 2021-03-31 15:57:23 +02:00
21728a663d chore: update README.md 2021-03-31 10:59:19 +02:00
9ca03f4625 feat(PostgreSQL): indexes management 2021-03-30 19:07:04 +02:00
affb7288b0 chore: appx logos 2021-03-30 19:06:48 +02:00
614e0d3275 feat(PostgreSQL): unique keys management 2021-03-29 20:18:44 +02:00
feef5e30ee feat(PostgreSQL): tables addition 2021-03-28 11:55:15 +02:00
82c25711b6 ci: moving to GitHub actions 2021-03-26 18:03:44 +01:00
2ca2988832 chore: appx build configuration 2021-03-26 16:52:49 +01:00
e3f259c6e8 feat(PostgreSQL): table fields edit 2021-03-25 18:33:29 +01:00
e7401cc96e fix: fields of ref. table not automatically loaded in foreign keys modal 2021-03-22 18:04:19 +01:00
13b9840f3d chore: update README.md 2021-03-21 14:39:11 +01:00
d20414b692 chore(release): 0.1.0 2021-03-21 13:01:35 +01:00
22a8c25717 fix: update or delete rows with more than one primary key 2021-03-21 13:00:27 +01:00
db47b4040a fix(PostgreSQL): issue getting foreign keys informations 2021-03-21 11:51:22 +01:00
e89911b185 fix: remove last char from datetime and time if is a dot 2021-03-20 16:29:56 +01:00
fccfe92453 fix(PostgreSQL): various issues in query results 2021-03-19 18:49:26 +01:00
d465e18dba feat(PostgreSQL): support to microseconds 2021-03-18 15:56:52 +01:00
ffb1712a59 feat(UI): support to boolean fields 2021-03-18 12:59:46 +01:00
9f6a183d9b fix(PostgreSQL): single quote escape 2021-03-18 12:30:06 +01:00
1f80a64fe1 feat(PostgreSQL): insert and edit blob fields 2021-03-18 11:09:50 +01:00
fc651149b9 feat(PostgreSQL): edit array and text search fields 2021-03-17 18:06:17 +01:00
964570247f feat(PostgreSQL): database in connection parameters 2021-03-17 16:51:26 +01:00
8a6c59f7ce fix: schema content not loaded if selected with right click 2021-03-17 11:57:47 +01:00
4d844fe2c9 refactor: rename database to schema 2021-03-17 11:15:14 +01:00
d892fa6fb3 feat(PostgreSQL): partial postgre implementation 2021-03-16 18:42:03 +01:00
8c9e4f6e96 chore: update issue templates 2021-03-16 15:55:11 +01:00
966ca60c89 chore: update README.md 2021-03-16 15:51:21 +01:00
9bbe218f90 chore: update README.md 2021-03-14 15:38:55 +01:00
a1c6be372b fix(MySQL): handle NEWDECIMAL data type 2021-03-14 15:04:20 +01:00
7d0c929fb8 chore(release): 0.0.20 2021-03-13 19:30:57 +01:00
25d72e3952 fix(UI): row mark not applied on first click 2021-03-10 15:55:34 +01:00
b232a3bb5f feat(UI): loader layers on query and data tabs 2021-03-09 19:14:02 +01:00
e9a26c1bc0 fix(UI): avoid unnecessary updates when cell content not change 2021-03-09 18:08:57 +01:00
76c5c0c680 fix(UI): table rows lose internal id after an update 2021-03-09 18:07:48 +01:00
ddfb713124 feat(UI): row markers in sql editors 2021-03-08 18:11:00 +01:00
0081a4167c refactor: moving from keytar to local storage due issues on Linux 2021-03-08 17:35:43 +01:00
239cb4488f ci: snap store config 2021-03-08 12:40:56 +01:00
fb5adbe676 chore: update package.json to support Windows portable build 2021-03-06 17:29:00 +01:00
9cd51c8d8b chore: update package.json 2021-03-06 17:11:03 +01:00
8dfaa3b7be chore(release): 0.0.19 2021-03-05 17:29:12 +01:00
5d7efa75b7 fix(UI): modal processes list does not regain size on window resize 2021-03-05 17:23:13 +01:00
4862d51fba fix(UI): wrong height in scrolling tables in some cases 2021-03-05 17:10:52 +01:00
07f60c3917 feat(UI): modal that shows process query 2021-03-04 19:34:18 +01:00
049143d143 feat: processes list tool 2021-03-03 19:31:05 +01:00
db4430609e feat(MySQL): support to new mysql8 authentication, closes #45 2021-03-02 12:03:01 +01:00
71b4310117 feat: context menu shortcut to set NULL a table cell 2021-02-28 21:45:38 +01:00
201fad9265 fix(MySQL): wrong TIMESTAMP fields length 2021-02-27 18:30:34 +01:00
45351faeae feat(UI): esc key to cancel cell edit 2021-02-27 18:29:47 +01:00
b4ead6992c perf(UI): improvements of date time inputs 2021-02-27 17:52:54 +01:00
b1ea32b680 feat: setting to enable beta updates (future use) 2021-02-27 17:28:01 +01:00
39ca1974bc perf(UI): big performance improvement in tables rendering 2021-02-26 22:31:05 +01:00
777b73fa6f feat(UI): query duration calc 2021-02-26 18:45:00 +01:00
4494e637f7 chore(release): 0.0.18 2021-02-25 19:09:02 +01:00
219da0aba4 feat(UI): run procedures/functions from sidebar context menu 2021-02-25 17:43:23 +01:00
7e8167154f feat(UI): run routines/functions from settings tab 2021-02-25 12:39:50 +01:00
3aa2159a1a fix(MySQL): issue obtaining routine/function parameters 2021-02-24 19:45:27 +01:00
76d92cd106 fix: issue managing function/routine parameters 2021-02-24 12:46:31 +01:00
c8545a250b fix(UI): elements from previous selected schemas in query suggestions 2021-02-22 19:14:02 +01:00
dbab06fcb8 fix(UI): data tab opened when non-table element is selected 2021-02-22 11:10:04 +01:00
b54fefbf25 feat(UI): context menu for input and textarea tags 2021-02-21 21:24:25 +01:00
9a1bf32128 feat(UI): html, xml, json, svg and yaml editor modes in long text fields edit 2021-02-21 19:22:03 +01:00
110b0b414c feat(UI): sticky schema names in explore bar 2021-02-20 18:55:08 +01:00
2f58007af4 feat(UI): search filter in explore bar 2021-02-20 11:55:34 +01:00
9b60bfff8d build: dropped use of lodash 2021-02-19 17:41:33 +01:00
3b37b7432e fix: disabled sort for fields without a name property 2021-02-18 18:12:36 +01:00
7c4ca999ce fix: prevents F5 shortcut to run in non-selected workspaces 2021-02-18 18:01:12 +01:00
94c4952319 fix: support of bit fields in table filler 2021-02-18 15:26:17 +01:00
99 changed files with 5409 additions and 766 deletions

View File

@@ -3,7 +3,7 @@ name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
assignees: Fabio286
---
@@ -25,13 +25,6 @@ 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**

View File

@@ -3,7 +3,7 @@ name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
assignees: Fabio286
---

26
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Build/release
on: push
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v1
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 10
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
github_token: ${{ secrets.github_token }}
release: ${{ startsWith(github.ref, 'refs/tags/v') }}

View File

@@ -8,7 +8,8 @@
"stylelint-scss"
],
"rules": {
"at-rule-no-unknown": null
"at-rule-no-unknown": null,
"no-descending-specificity": null
},
"syntax": "scss"
}

View File

@@ -2,6 +2,114 @@
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.1.1](https://github.com/Fabio286/antares/compare/v0.1.0...v0.1.1) (2021-04-03)
### Features
* scratchpad to save persistent notes ([e349dd5](https://github.com/Fabio286/antares/commit/e349dd5eaba608591257f2799b830805e4936c27))
* **PostgreSQL:** foreign keys management ([fe4c8e1](https://github.com/Fabio286/antares/commit/fe4c8e12b39dd3cdfc233f07e3fe2ff0676252b0))
* **PostgreSQL:** indexes management ([9ca03f4](https://github.com/Fabio286/antares/commit/9ca03f462560b634970a19d3d97b878d60509acc))
* **PostgreSQL:** table fields edit ([e3f259c](https://github.com/Fabio286/antares/commit/e3f259c6e8327d71bd7dd0a9c33d957dc6ca1fb8))
* **PostgreSQL:** tables addition ([feef5e3](https://github.com/Fabio286/antares/commit/feef5e30eec915cbb219223cc428bd4e98d2e9c5))
* **PostgreSQL:** unique keys management ([614e0d3](https://github.com/Fabio286/antares/commit/614e0d32758c13b59139d349d4682a5bafc3ca88))
* **PostgreSQL:** views management ([99f7511](https://github.com/Fabio286/antares/commit/99f7511c4d5fab4030b30d5134cd03248167faea))
* **UI:** light theme ([2806976](https://github.com/Fabio286/antares/commit/280697698ea5fae6d54326970c823878888c196c))
### Bug Fixes
* **UI:** editor theme preview not properly loaded in some cases ([c981244](https://github.com/Fabio286/antares/commit/c981244d7aa93ca18ca2de44bf8df06f253b9d20))
* fields of ref. table not automatically loaded in foreign keys modal ([e7401cc](https://github.com/Fabio286/antares/commit/e7401cc96e76e00100a88eea9f40541fd8027adb))
* hide update tab for Windows Store distributions ([dcb135d](https://github.com/Fabio286/antares/commit/dcb135dd015b8f8c6cfb44021211bb8cf3089192))
## [0.1.0](https://github.com/Fabio286/antares/compare/v0.0.20...v0.1.0) (2021-03-21)
### Features
* **PostgreSQL:** database in connection parameters ([9645702](https://github.com/Fabio286/antares/commit/964570247ff5b7b8317419730eec5bed4f0f0580))
* **PostgreSQL:** edit array and text search fields ([fc65114](https://github.com/Fabio286/antares/commit/fc651149b95399c52d2d63e946731e9c1b0303a9))
* **PostgreSQL:** insert and edit blob fields ([1f80a64](https://github.com/Fabio286/antares/commit/1f80a64fe1400baacca26f1a762c5aeb4ef6350d))
* **PostgreSQL:** partial postgre implementation ([d892fa6](https://github.com/Fabio286/antares/commit/d892fa6fb3e86fbb96887d8eb67319ae855260a1))
* **PostgreSQL:** support to microseconds ([d465e18](https://github.com/Fabio286/antares/commit/d465e18dba8ea3aa00726e33f9b1f70ca4c0683c))
* **UI:** support to boolean fields ([ffb1712](https://github.com/Fabio286/antares/commit/ffb1712a593d1421793011e48a17369b884ea3c0))
### Bug Fixes
* update or delete rows with more than one primary key ([22a8c25](https://github.com/Fabio286/antares/commit/22a8c25717a4d4b285855426098a3a2846ce7448))
* **MySQL:** handle NEWDECIMAL data type ([a1c6be3](https://github.com/Fabio286/antares/commit/a1c6be372b570cf13e89ef7ecf9b7a7c033a9293))
* **PostgreSQL:** issue getting foreign keys informations ([db47b40](https://github.com/Fabio286/antares/commit/db47b4040a5282a6ac0711b1926c4c2ac867999e))
* remove last char from datetime and time if is a dot ([e89911b](https://github.com/Fabio286/antares/commit/e89911b1851c19813d4acf2c79adfbc2ac7c1112))
* **PostgreSQL:** single quote escape ([9f6a183](https://github.com/Fabio286/antares/commit/9f6a183d9b293dfe9ad9f3759f2375f05f37db8e))
* **PostgreSQL:** various issues in query results ([fccfe92](https://github.com/Fabio286/antares/commit/fccfe92453325cd54c0331cc5670af0a56822c5b))
* schema content not loaded if selected with right click ([8a6c59f](https://github.com/Fabio286/antares/commit/8a6c59f7ce7d051315b04cea38a96e4739b5b9d3))
### [0.0.20](https://github.com/Fabio286/antares/compare/v0.0.19...v0.0.20) (2021-03-13)
### Features
* **UI:** loader layers on query and data tabs ([b232a3b](https://github.com/Fabio286/antares/commit/b232a3bb5ff7e38c83aa33e8b96ec7202bc4881e))
* **UI:** row markers in sql editors ([ddfb713](https://github.com/Fabio286/antares/commit/ddfb7131248a47fa2055ccfb72e223986a17f986))
### Bug Fixes
* **UI:** avoid unnecessary updates when cell content not change ([e9a26c1](https://github.com/Fabio286/antares/commit/e9a26c1bc0641b7087d8143cc948405850d7552f))
* **UI:** row mark not applied on first click ([25d72e3](https://github.com/Fabio286/antares/commit/25d72e39529884c09cf1286ff64ba00c8a5c7b24))
* **UI:** table rows lose internal id after an update ([76c5c0c](https://github.com/Fabio286/antares/commit/76c5c0c680521b4a20de1f12bab25314ac084d5c))
### [0.0.19](https://github.com/Fabio286/antares/compare/v0.0.18...v0.0.19) (2021-03-05)
### Features
* **UI:** modal that shows process query ([07f60c3](https://github.com/Fabio286/antares/commit/07f60c39173e9db452909d74573c3aecf4b6466b))
* processes list tool ([049143d](https://github.com/Fabio286/antares/commit/049143d143d8187bfcb6377f2bf374c471c28046))
* **MySQL:** support to new mysql8 authentication, closes [#45](https://github.com/Fabio286/antares/issues/45) ([db44306](https://github.com/Fabio286/antares/commit/db4430609e22816f4a3a8ecdbf61e9b51cde2579))
* context menu shortcut to set NULL a table cell ([71b4310](https://github.com/Fabio286/antares/commit/71b43101172c27d810d533c936534bcf089dbca2))
* **UI:** esc key to cancel cell edit ([45351fa](https://github.com/Fabio286/antares/commit/45351faeaea6ad4af8280da6c0ec1b4ded0f86fe))
* setting to enable beta updates (future use) ([b1ea32b](https://github.com/Fabio286/antares/commit/b1ea32b68024593481120e2ef67642e127554888))
* **UI:** query duration calc ([777b73f](https://github.com/Fabio286/antares/commit/777b73fa6f9d6f7806e0d3d07589dfaee0c40786))
### Bug Fixes
* **MySQL:** wrong TIMESTAMP fields length ([201fad9](https://github.com/Fabio286/antares/commit/201fad9265e01e19ae4c8dc16bff84ee9dc9c894))
* **UI:** modal processes list does not regain size on window resize ([5d7efa7](https://github.com/Fabio286/antares/commit/5d7efa75b76f59b85654603b4df8035a36af0576))
* **UI:** wrong height in scrolling tables in some cases ([4862d51](https://github.com/Fabio286/antares/commit/4862d51fba863e055ca6735586b0abe4894037eb))
### Improvements
* **UI:** big performance improvement in tables rendering ([39ca197](https://github.com/Fabio286/antares/commit/39ca1974bcea84c9047779893ed3f458300474e3))
* **UI:** improvements of date time inputs ([b4ead69](https://github.com/Fabio286/antares/commit/b4ead6992c8ddc172380a97e7aa125e0dd078f1e))
### [0.0.18](https://github.com/Fabio286/antares/compare/v0.0.17...v0.0.18) (2021-02-25)
### Features
* **UI:** context menu for input and textarea tags ([b54fefb](https://github.com/Fabio286/antares/commit/b54fefbf255c91f3cdd0c52b917b239ea40dae16))
* **UI:** html, xml, json, svg and yaml editor modes in long text fields edit ([9a1bf32](https://github.com/Fabio286/antares/commit/9a1bf3212850d3783ac6382f8a980eb25c4f4226))
* **UI:** run procedures/functions from sidebar context menu ([219da0a](https://github.com/Fabio286/antares/commit/219da0aba46839295807a01056199d42584e0af9))
* **UI:** run routines/functions from settings tab ([7e81671](https://github.com/Fabio286/antares/commit/7e8167154ffd64e46d78a50cd13cf90cee61370c))
* **UI:** search filter in explore bar ([2f58007](https://github.com/Fabio286/antares/commit/2f58007af4f32207fe4be9fc15bb43916e8e0abc))
* **UI:** sticky schema names in explore bar ([110b0b4](https://github.com/Fabio286/antares/commit/110b0b414cef25395ae28493032c2048307e4783))
### Bug Fixes
* **MySQL:** issue obtaining routine/function parameters ([3aa2159](https://github.com/Fabio286/antares/commit/3aa2159a1ae3b6785646fc7ac1e866cb0277d087))
* issue managing function/routine parameters ([76d92cd](https://github.com/Fabio286/antares/commit/76d92cd106b0409e8752c43d7d587d09dd3e1e32))
* **UI:** data tab opened when non-table element is selected ([dbab06f](https://github.com/Fabio286/antares/commit/dbab06fcb80ccdae85215ceae9ba24af3e7ff263))
* **UI:** elements from previous selected schemas in query suggestions ([c8545a2](https://github.com/Fabio286/antares/commit/c8545a250bc696c339e418a69a2942d5bac10cbf))
* disabled sort for fields without a name property ([3b37b74](https://github.com/Fabio286/antares/commit/3b37b7432e969e6315ebd6ed74905ec3980a049c))
* prevents F5 shortcut to run in non-selected workspaces ([7c4ca99](https://github.com/Fabio286/antares/commit/7c4ca999ce64af8077d29e419f430d5beaaead93))
* support of bit fields in table filler ([94c4952](https://github.com/Fabio286/antares/commit/94c49523197284510361ebbb6984816a3bb1b243))
### [0.0.17](https://github.com/Fabio286/antares/compare/v0.0.16...v0.0.17) (2021-02-17)

View File

@@ -1,19 +1,19 @@
<p align="center">
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/screen-alpha.png">
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/gh-logo.png">
</p>
# Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares)
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares)
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is an alpha, it lacks many features** and supports only MySQL.
Most of its current features might be enough for basic MySQL use, so give it a chance and send me your feedback, I would really appreciate it.
I'm actively working on it (yes, i'm a lone dev), hoping to provide cool features and fixes as soon as possible.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL (partially).
Many of its current features are enough to have a pleasant user experience with MySQL/MariaDB, and basic functionalites with PostgreSQL, so give it a chance and send me your feedback, I would really appreciate it.
I'm actively working on it, hoping to provide cool features and fixes as soon as possible.
🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases).
🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest).
👁 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.
@@ -21,7 +21,12 @@ I'm actively working on it (yes, i'm a lone dev), hoping to provide cool feature
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, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem.
An application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons or submenu.
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, tabs or submenu.
## Download
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/antares) [![Get it from Microsoft Store](https://raw.githubusercontent.com/Fabio286/antares/gh-pages/src/assets/ms-store.png)](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
🚀 **[Other Downloads](https://github.com/Fabio286/antares/releases/latest)**
## How to contribute
@@ -36,7 +41,8 @@ An application created with minimalism and semplicity in mind, with features in
- Fake table data filler.
- Run queries on multiple tabs.
- Query suggestions and auto complete.
- Native dark theme.
- Dark and light theme.
- Scratchpad.
- Multi language.
- Secure password storage.
- Auto updates.
@@ -46,7 +52,7 @@ An application created with minimalism and semplicity in mind, with features in
This is a roadmap with major features will come in near future.
- Support for other databases.
- Database tools (variables, process list...).
- Database tools.
- SSH tunnel support.
- Users management (add/edit/delete).
- UI/UX improvements.
@@ -55,7 +61,6 @@ This is a roadmap with major features will come in near future.
- More keyboard shortcuts.
- Query logs console.
- Import/export and migration.
- Light theme.
## Troubleshooting
@@ -73,9 +78,9 @@ Depending on your distribution, you will need to run the following command:
### Databases
- [x] MySQL/MariaDB
- [ ] PostgreSQL
- [ ] MSSQL
- [x] PostgreSQL (partially, work in progress)
- [ ] SQLite
- [ ] MSSQL
- [ ] OracleDB
- [ ] More...
@@ -85,7 +90,7 @@ Depending on your distribution, you will need to run the following command:
- [x] Windows
- [x] Linux
- [x] MacOS (i need feedbacks)
- [x] MacOS (not tested due lack of hardware)
#### • ARM

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
build/appx/StoreLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/gh-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.0.17",
"version": "0.1.1",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
@@ -9,6 +9,7 @@
"dev": "cross-env NODE_ENV=development electron-webpack dev",
"compile": "electron-webpack",
"build": "cross-env NODE_ENV=production npm run compile && electron-builder",
"build:appx": "npm run build -- --win appx",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha",
"test": "npm run lint",
@@ -19,6 +20,12 @@
"build": {
"appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"win": {
"target": [
"nsis",
"portable"
]
},
"dmg": {
"contents": [
{
@@ -43,6 +50,15 @@
"appImage": {
"license": "./LICENSE",
"category": "Development"
},
"portable": {
"artifactName": "${productName}-${version}-portable.exe"
},
"appx": {
"displayName": "Antares SQL Client",
"identityName": "62514FabioDiStasio.AntaresSQLClient",
"publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52",
"applicationId": "FabioDiStasio.AntaresSQLClient"
}
},
"electronWebpack": {
@@ -58,38 +74,39 @@
"electron-updater": "^4.3.5",
"faker": "^5.3.1",
"keytar": "^7.3.0",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"mssql": "^6.2.3",
"mysql": "^2.18.1",
"mysql2": "^2.2.5",
"node-sql-parser": "^3.1.0",
"pg": "^8.5.1",
"pgsql-ast-parser": "^7.0.2",
"source-map-support": "^0.5.16",
"spectre.css": "^0.5.9",
"v-mask": "^2.2.4",
"vue-i18n": "^8.22.4",
"vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.2",
"electron": "^11.2.1",
"electron": "^11.3.0",
"electron-builder": "^22.9.1",
"electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.18.0",
"eslint": "^7.20.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-vue": "^7.5.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-vue": "^7.6.0",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.1",
"standard-version": "^9.1.0",
"stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"stylelint-scss": "^3.19.0",
"vue": "^2.6.12",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.46.0"

96
snap/snapcraft.yaml Normal file
View File

@@ -0,0 +1,96 @@
name: antares
adopt-info: antares
summary: Open source SQL client made to be simple and complete.
description: |
Antares is an SQL client that aims to become an useful and complete tool, especially for developers.
The target is to support as many databases as possible, and all major operating systems, including the ARM versions.
At the moment this application is an alpha and supports only MySQL and x86 architecture.
Most of its current features might be enough for MySQL management, so give it a chance and send us your feedback, we would really appreciate it.
base: core18
grade: stable
confinement: strict
architectures:
- build-on: amd64
compression: lzo
parts:
antares:
plugin: dump
source: .
override-build: |
snapcraftctl build
ARCHITECTURE=$(dpkg --print-architecture)
if [ "${ARCHITECTURE}" = "amd64" ]; then
FILTER="amd64.deb"
else
echo "ERROR! Antares only produces debs for amd64. Failing the build here."
exit 1
fi
# Get the latest releases json
echo "Get GitHub releases..."
wget --quiet https://api.github.com/repos/fabio286/antares/releases/latest -O releases.json
# Get the version from the tag_name and the download URL.
VERSION=$(jq . releases.json | grep tag_name | cut -d'"' -f4 | sed s'/release-//')
DEB_URL=$(cat releases.json | jq -r ".assets[] | select(.name | test(\"${FILTER}\")) | .browser_download_url")
DEB=$(basename "${DEB_URL}")
echo "Downloading ${DEB_URL}..."
wget --quiet "${DEB_URL}" -O "${SNAPCRAFT_PART_INSTALL}/${DEB}"
echo "Unpacking ${DEB}..."
dpkg -x "${SNAPCRAFT_PART_INSTALL}/${DEB}" ${SNAPCRAFT_PART_INSTALL}
rm -f releases.json 2>/dev/null
rm -f "${SNAPCRAFT_PART_INSTALL}/${DEB}" 2>/dev/null
echo $VERSION > $SNAPCRAFT_STAGE/version
# Correct path to icon.
sed -i 's|Icon=antares|Icon=/usr/share/icons/hicolor/256x256/apps/antares\.png|g' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/antares.desktop
# Delete usr/bin/antares, it's a broken symlink pointing outside the snap.
rm -f ${SNAPCRAFT_PART_INSTALL}/usr/bin/antares
chmod -s ${SNAPCRAFT_PART_INSTALL}/opt/Antares/chrome-sandbox
snapcraftctl set-version "$(echo $VERSION)"
build-packages:
- dpkg
- jq
- sed
- wget
stage-packages:
- fcitx-frontend-gtk3
- libappindicator3-1
- libasound2
- libgconf-2-4
- libgtk-3-0
- libnotify4
- libnspr4
- libnss3
- libpcre3
- libpulse0
- libxss1
- libsecret-1-0
- libxtst6
- libxkbfile1
cleanup:
after: [antares]
plugin: nil
build-snaps: [ gnome-3-28-1804 ]
override-prime: |
set -eux
cd /snap/gnome-3-28-1804/current
find . -type f,l -exec rm -f $SNAPCRAFT_PRIME/{} \;
apps:
antares:
command: opt/Antares/antares --no-sandbox
desktop: usr/share/applications/antares.desktop
extensions: [gnome-3-28]
environment:
# Fallback to XWayland if running in a Wayland session.
DISABLE_WAYLAND: 1
plugs:
- browser-support
- cups-control
- home
- network
- opengl
- pulseaudio
- removable-media
- unity7

View File

@@ -0,0 +1,52 @@
module.exports = {
// Defaults
defaultPort: null,
defaultUser: null,
defaultDatabase: null,
// Core
database: false,
collations: false,
engines: false,
// Tools
processesList: false,
usersManagement: false,
variables: false,
// Structure
schemas: false,
tables: false,
views: false,
triggers: false,
routines: false,
functions: false,
schedulers: false,
// Settings
tableAdd: false,
viewAdd: false,
triggerAdd: false,
routineAdd: false,
functionAdd: false,
schedulerAdd: false,
databaseEdit: false,
schemaEdit: false,
tableSettings: false,
viewSettings: false,
triggerSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
indexes: false,
foreigns: false,
sortableFields: false,
unsigned: false,
nullable: false,
zerofill: false,
autoIncrement: false,
comment: false,
collation: false,
definer: false,
arrays: false,
onUpdate: false,
viewAlgorithm: false,
viewSqlSecurity: false,
viewUpdateOption: false
};

View File

@@ -0,0 +1,5 @@
module.exports = {
maria: require('./mysql'),
mysql: require('./mysql'),
pg: require('./postgresql')
};

View File

@@ -0,0 +1,50 @@
const defaults = require('./defaults');
module.exports = {
...defaults,
// Defaults
defaultPort: 3306,
defaultUser: 'root',
defaultDatabase: null,
// Core
collations: true,
engines: true,
// Tools
processesList: true,
// Structure
schemas: true,
tables: true,
views: true,
triggers: true,
routines: true,
functions: true,
schedulers: true,
// Settings
tableAdd: true,
viewAdd: true,
triggerAdd: true,
routineAdd: true,
functionAdd: true,
schedulerAdd: true,
schemaEdit: true,
tableSettings: true,
viewSettings: true,
triggerSettings: true,
routineSettings: true,
functionSettings: true,
schedulerSettings: true,
indexes: true,
foreigns: true,
sortableFields: true,
unsigned: true,
nullable: true,
zerofill: true,
autoIncrement: true,
comment: true,
collation: true,
definer: true,
onUpdate: true,
viewAlgorithm: true,
viewSqlSecurity: true,
viewUpdateOption: true
};

View File

@@ -0,0 +1,35 @@
const defaults = require('./defaults');
module.exports = {
...defaults,
// Defaults
defaultPort: 5432,
defaultUser: 'postgres',
defaultDatabase: 'postgres',
// Core
database: true,
// Tools
processesList: true,
// Structure
tables: true,
views: true,
triggers: false,
routines: false,
functions: false,
schedulers: false,
// Settings
tableAdd: true,
viewAdd: true,
databaseEdit: false,
tableSettings: true,
viewSettings: true,
triggerSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
indexes: true,
foreigns: true,
sortableFields: false,
nullable: true,
arrays: true
};

View File

@@ -4,7 +4,7 @@ module.exports = [
types: [
{
name: 'TINYINT',
length: true,
length: 4,
collation: false,
unsigned: true,
zerofill: true

View File

@@ -0,0 +1,292 @@
module.exports = [
{
group: 'integer',
types: [
{
name: 'SMALLINT',
length: false,
unsigned: true
},
{
name: 'INTEGER',
length: false,
unsigned: true
},
{
name: 'BIGINT',
length: false,
unsigned: true
},
{
name: 'DECIMAL',
length: false,
unsigned: true
},
{
name: 'NUMERIC',
length: true,
unsigned: true
},
{
name: 'SMALLSERIAL',
length: false,
unsigned: true
},
{
name: 'SERIAL',
length: false,
unsigned: true
},
{
name: 'BIGSERIAL',
length: false,
unsigned: true
}
]
},
{
group: 'float',
types: [
{
name: 'REAL',
length: false,
unsigned: true
},
{
name: 'DOUBLE PRECISION',
length: false,
unsigned: true
}
]
},
{
group: 'monetary',
types: [
{
name: 'money',
length: false,
unsigned: true
}
]
},
{
group: 'string',
types: [
{
name: 'CHARACTER VARYING',
length: true,
unsigned: false
},
{
name: 'CHARACTER',
length: true,
unsigned: false
},
{
name: 'TEXT',
length: false,
unsigned: false
},
{
name: '"CHAR"',
length: false,
unsigned: false
},
{
name: 'NAME',
length: false,
unsigned: false
}
]
},
{
group: 'binary',
types: [
{
name: 'BYTEA',
length: false,
unsigned: false
}
]
},
{
group: 'time',
types: [
{
name: 'TIMESTAMP WITHOUT TIME ZONE',
length: false,
unsigned: false
},
{
name: 'TIMESTAMP WITH TIME ZONE',
length: false,
unsigned: false
},
{
name: 'DATE',
length: false,
unsigned: false
},
{
name: 'TIME WITHOUT TIME ZONE',
length: false,
unsigned: false
},
{
name: 'TIME WITH TIME ZONE',
length: false,
unsigned: false
},
{
name: 'INTERVAL',
length: false,
unsigned: false
}
]
},
{
group: 'boolean',
types: [
{
name: 'BOOLEAN',
length: false,
unsigned: false
}
]
},
{
group: 'geometric',
types: [
{
name: 'POINT',
length: false,
unsigned: false
},
{
name: 'LINE',
length: false,
unsigned: false
},
{
name: 'LSEG',
length: false,
unsigned: false
},
{
name: 'BOX',
length: false,
unsigned: false
},
{
name: 'PATH',
length: false,
unsigned: false
},
{
name: 'POLYGON',
length: false,
unsigned: false
},
{
name: 'CIRCLE',
length: false,
unsigned: false
}
]
},
{
group: 'network',
types: [
{
name: 'CIDR',
length: false,
unsigned: false
},
{
name: 'INET',
length: false,
unsigned: false
},
{
name: 'MACADDR',
length: false,
unsigned: false
},
{
name: 'MACADDR8',
length: false,
unsigned: false
}
]
},
{
group: 'bit',
types: [
{
name: 'BIT',
length: true,
unsigned: false
},
{
name: 'BIT VARYING',
length: true,
unsigned: false
}
]
},
{
group: 'text search',
types: [
{
name: 'TSVECTOR',
length: false,
unsigned: false
},
{
name: 'TSQUERY',
length: false,
unsigned: false
}
]
},
{
group: 'uuid',
types: [
{
name: 'UUID',
length: false,
unsigned: false
}
]
},
{
group: 'xml',
types: [
{
name: 'XML',
length: false,
unsigned: false
}
]
},
{
group: 'json',
types: [
{
name: 'JSON',
length: false,
unsigned: false
},
{
name: 'JSONB',
length: false,
unsigned: false
},
{
name: 'JSONPATH',
length: false,
unsigned: false
}
]
}
];

View File

@@ -1,13 +1,75 @@
export const TEXT = ['CHAR', 'VARCHAR'];
export const LONG_TEXT = ['TEXT', 'MEDIUMTEXT', 'LONGTEXT'];
export const TEXT = [
'CHAR',
'VARCHAR',
'CHARACTER',
'CHARACTER VARYING'
];
export const LONG_TEXT = [
'TEXT',
'MEDIUMTEXT',
'LONGTEXT'
];
export const NUMBER = ['INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'BIGINT', 'DECIMAL', 'BOOL'];
export const FLOAT = ['FLOAT', 'DOUBLE'];
export const ARRAY = [
'ARRAY',
'ANYARRAY'
];
export const TEXT_SEARCH = [
'TSVECTOR',
'TSQUERY'
];
export const NUMBER = [
'INT',
'TINYINT',
'SMALLINT',
'MEDIUMINT',
'BIGINT',
'DECIMAL',
'NUMERIC',
'INTEGER',
'SMALLSERIAL',
'SERIAL',
'BIGSERIAL',
'OID',
'XID'
];
export const FLOAT = [
'FLOAT',
'DOUBLE',
'REAL',
'DOUBLE PRECISION',
'MONEY'
];
export const BOOLEAN = [
'BOOL',
'BOOLEAN'
];
export const DATE = ['DATE'];
export const TIME = ['TIME'];
export const DATETIME = ['DATETIME', 'TIMESTAMP'];
export const TIME = [
'TIME',
'TIME WITH TIME ZONE'
];
export const DATETIME = [
'DATETIME',
'TIMESTAMP',
'TIMESTAMP WITHOUT TIME ZONE',
'TIMESTAMP WITH TIME ZONE'
];
export const BLOB = ['BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB'];
export const BLOB = [
'BLOB',
'TINYBLOB',
'MEDIUMBLOB',
'LONGBLOB',
'BYTEA'
];
export const BIT = ['BIT'];
export const BIT = [
'BIT',
'BIT VARYING'
];

View File

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

View File

@@ -96,11 +96,16 @@ else {
// create main BrowserWindow when electron is ready
app.on('ready', async () => {
let key = await keytar.getPassword('antares', 'user');
try {
let key = await keytar.getPassword('antares', 'user');
if (!key) {
key = crypto.randomBytes(16).toString('hex');
keytar.setPassword('antares', 'user', key);
if (!key) {
key = crypto.randomBytes(16).toString('hex');
keytar.setPassword('antares', 'user', key);
}
}
catch (err) {
console.log(err);
}
mainWindow = createMainWindow();

View File

@@ -7,7 +7,14 @@ export default () => {
});
ipcMain.on('get-key', async event => {
const key = await keytar.getPassword('antares', 'user');
let key = false;
try {
key = await keytar.getPassword('antares', 'user');
}
catch (err) {
console.log(err);
}
event.returnValue = key;
});
};

View File

@@ -11,6 +11,9 @@ export default connections => {
password: conn.password
};
if (conn.database)
params.database = conn.database;
if (conn.ssl) {
params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null,
@@ -50,6 +53,9 @@ export default connections => {
password: conn.password
};
if (conn.database)
params.database = conn.database;
if (conn.ssl) {
params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null,
@@ -59,13 +65,13 @@ export default connections => {
};
}
const connection = ClientsFactory.getConnection({
client: conn.client,
params,
poolSize: 1
});
try {
const connection = ClientsFactory.getConnection({
client: conn.client,
params,
poolSize: 1
});
await connection.connect();
const structure = await connection.getStructure(new Set());

View File

@@ -7,7 +7,7 @@ import functions from './functions';
import schedulers from './schedulers';
import updates from './updates';
import application from './application';
import database from './database';
import schema from './schema';
import users from './users';
const connections = {};
@@ -20,7 +20,7 @@ export default () => {
routines(connections);
functions(connections);
schedulers(connections);
database(connections);
schema(connections);
users(connections);
updates();
application();

View File

@@ -2,10 +2,9 @@
import { ipcMain } from 'electron';
export default connections => {
ipcMain.handle('create-database', async (event, params) => {
ipcMain.handle('create-schema', async (event, params) => {
try {
const query = `CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`;
await connections[params.uid].raw(query);
await connections[params.uid].createSchema(params);
return { status: 'success' };
}
@@ -14,10 +13,9 @@ export default connections => {
}
});
ipcMain.handle('update-database', async (event, params) => {
ipcMain.handle('update-schema', async (event, params) => {
try {
const query = `ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`;
await connections[params.uid].raw(query);
await connections[params.uid].alterSchema(params);
return { status: 'success' };
}
@@ -26,10 +24,9 @@ export default connections => {
}
});
ipcMain.handle('delete-database', async (event, params) => {
ipcMain.handle('delete-schema', async (event, params) => {
try {
const query = `DROP DATABASE \`${params.database}\``;
await connections[params.uid].raw(query);
await connections[params.uid].dropSchema(params);
return { status: 'success' };
}
@@ -38,10 +35,9 @@ export default connections => {
}
});
ipcMain.handle('get-database-collation', async (event, params) => { // TODO: move to mysql class
ipcMain.handle('get-schema-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);
const collation = await connections[params.uid].getDatabaseCollation(params);
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
}
@@ -105,6 +101,17 @@ export default connections => {
}
});
ipcMain.handle('get-processes', async (event, uid) => {
try {
const result = await connections[uid].getProcesses();
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('use-schema', async (event, { uid, schema }) => {
if (!schema) return;

View File

@@ -2,7 +2,7 @@ import { ipcMain } from 'electron';
import faker from 'faker';
import moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import fs from 'fs';
export default (connections) => {
@@ -59,55 +59,91 @@ export default (connections) => {
});
ipcMain.handle('update-table-cell', async (event, params) => {
try {
try { // TODO: move to client classes
let escapedParam;
let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
if ([...NUMBER, ...FLOAT].includes(params.type))
escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type))
escapedParam = `"${sqlEscaper(params.content)}"`;
else if ([...TEXT, ...LONG_TEXT].includes(params.type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.content)}"`;
break;
case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (ARRAY.includes(params.type))
escapedParam = `'${params.content}'`;
else if (TEXT_SEARCH.includes(params.type))
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
else if (BLOB.includes(params.type)) {
if (params.content) {
const fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
reload = true;
}
else
escapedParam = '""';
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '\'\'';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
}
else if ([...BIT].includes(params.type)) {
escapedParam = `b'${sqlEscaper(params.content)}'`;
reload = true;
}
else if (params.content === null)
escapedParam = 'NULL';
else
escapedParam = `"${sqlEscaper(params.content)}"`;
escapedParam = `'${sqlEscaper(params.content)}'`;
if (params.primary) {
if (params.primary) { // TODO: handle multiple primary
await connections[params.uid]
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${id}` })
.limit(1)
.run();
}
else {
const { row } = params;
const { orgRow } = params;
reload = true;
for (const key in row) {
if (typeof row[key] === 'string')
row[key] = `'${row[key]}'`;
for (const key in orgRow) {
if (typeof orgRow[key] === 'string')
orgRow[key] = `'${orgRow[key]}'`;
row[key] = `= ${row[key]}`;
orgRow[key] = `= ${orgRow[key]}`;
}
await connections[params.uid]
.schema(params.schema)
.update({ [params.field]: `= ${escapedParam}` })
.from(params.table)
.where(row)
.where(orgRow)
.limit(1)
.run();
}
@@ -165,7 +201,7 @@ export default (connections) => {
});
ipcMain.handle('insert-table-rows', async (event, params) => {
try {
try { // TODO: move to client classes
const insertObj = {};
for (const key in params.row) {
const type = params.fields[key];
@@ -174,19 +210,46 @@ export default (connections) => {
if (params.row[key] === null)
escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].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')}`;
escapedParam = +params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break;
case 'pg':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (BLOB.includes(type)) {
if (params.row[key].value) {
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
}
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '""';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
else
escapedParam = '""';
}
else
escapedParam = `"${sqlEscaper(params.row[key])}"`;
insertObj[key] = escapedParam;
}
@@ -207,7 +270,7 @@ export default (connections) => {
});
ipcMain.handle('insert-table-fake-rows', async (event, params) => {
try {
try { // TODO: move to client classes
const rows = [];
for (let i = 0; i < +params.repeat; i++) {
@@ -222,18 +285,49 @@ export default (connections) => {
escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = params.row[key].value;
else if ([...TEXT, ...LONG_TEXT].includes(type))
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
else if ([...TEXT, ...LONG_TEXT].includes(type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break;
case 'pg':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (BLOB.includes(type)) {
if (params.row[key].value) {
const fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
}
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '""';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
else
escapedParam = '""';
}
else if (BIT.includes(type))
escapedParam = `b'${sqlEscaper(params.row[key].value)}'`;
else
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
escapedParam = `'${sqlEscaper(params.row[key].value)}'`;
insertObj[key] = escapedParam;
}
@@ -257,10 +351,10 @@ export default (connections) => {
if (typeof fakeValue === 'string') {
if (params.row[key].length)
fakeValue = fakeValue.substr(0, params.row[key].length);
fakeValue = `"${sqlEscaper(fakeValue)}"`;
fakeValue = `'${sqlEscaper(fakeValue)}'`;
}
else if ([...DATE, ...DATETIME].includes(type))
fakeValue = `"${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}"`;
fakeValue = `'${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}'`;
insertObj[key] = fakeValue;
}
@@ -285,13 +379,13 @@ export default (connections) => {
ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => {
try {
const query = connections[uid]
.select(`${column} AS foreignColumn`)
.select(`${column} AS foreign_column`)
.schema(schema)
.from(table)
.orderBy('foreignColumn ASC');
.orderBy('foreign_column ASC');
if (description)
query.select(`LEFT(${description}, 20) AS foreignDescription`);
query.select(`LEFT(${description}, 20) AS foreign_description`);
const results = await query.run();

View File

@@ -1,16 +1,21 @@
import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
let mainWindow;
autoUpdater.allowPrerelease = true;
autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
export default () => {
ipcMain.on('check-for-updates', event => {
mainWindow = event;
autoUpdater.checkForUpdatesAndNotify().catch(() => {
mainWindow.reply('check-failed');
});
if (process.windowsStore)
mainWindow.reply('no-auto-update');
else {
autoUpdater.checkForUpdatesAndNotify().catch(() => {
mainWindow.reply('check-failed');
});
}
});
ipcMain.on('restart-to-update', () => {

View File

@@ -1,5 +1,6 @@
'use strict';
import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
export class ClientsFactory {
/**
@@ -20,8 +21,10 @@ export class ClientsFactory {
case 'mysql':
case 'maria':
return new MySQLClient(args);
case 'pg':
return new PostgreSQLClient(args);
default:
return new Error(`Unknown database client: ${args.client}`);
throw new Error(`Unknown database client: ${args.client}`);
}
}
}

View File

@@ -1,5 +1,5 @@
'use strict';
import mysql from 'mysql';
import mysql from 'mysql2';
import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql';
@@ -43,17 +43,21 @@ export class MySQLClient extends AntaresCore {
}
_getType (field) {
let name = this.types[field.type];
let length = field.length;
let name = this.types[field.columnType];
let length = field.columnLength;
if (['DATE', 'TIME', 'YEAR', 'DATETIME'].includes(name))
length = field.decimals;
if (name === 'CHAR' && field.charsetNr === 63)// if binary
name = 'BINARY';
if (name === 'TIMESTAMP')
length = 0;
if (name === 'VARCHAR' && field.charsetNr === 63)// if binary
name = 'VARBINARY';
if (field.charsetNr === 63) { // if binary
if (name === 'CHAR')
name = 'BINARY';
else if (name === 'VARCHAR')
name = 'VARBINARY';
}
if (name === 'BLOB') {
switch (length) {
@@ -400,6 +404,44 @@ export class MySQLClient extends AntaresCore {
});
}
/**
* CREATE DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createSchema (params) {
return await this.raw(`CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`);
}
/**
* ALTER DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterSchema (params) {
return await this.raw(`ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`);
}
/**
* DROP DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropSchema (params) {
return await this.raw(`DROP DATABASE \`${params.database}\``);
}
/**
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async getDatabaseCollation (params) {
return await this.raw(`SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`);
}
/**
* SHOW CREATE VIEW
*
@@ -551,18 +593,18 @@ export class MySQLClient extends AntaresCore {
}
const parameters = row['Create Procedure']
.match(/(?<=\().*?(?=\))/s)[0]
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
const type = param[2] ? param[2].replace(')', '').split('(') : ['', null];
return {
name: param[1] ? param[1].replaceAll('`', '') : '',
type: type[0],
length: +type[1],
type: type[0].replaceAll('\n', ''),
length: +type[1] ? +type[1].replace(/\D/g, '') : '',
context: param[0] ? param[0].replace('\n', '') : ''
};
}).filter(el => el.name);
@@ -578,7 +620,7 @@ export class MySQLClient extends AntaresCore {
return {
definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters,
parameters: parameters || [],
name: row.Procedure,
comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
@@ -628,10 +670,12 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async createRoutine (routine) {
const parameters = routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
const parameters = 'parameters' in routine
? routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',')
: '';
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters})
LANGUAGE SQL
@@ -671,9 +715,10 @@ export class MySQLClient extends AntaresCore {
}
const parameters = row['Create Function']
.match(/(?<=\().*?(?=\))/s)[0]
.match(/(\([^()]*(?:(?:\([^()]*\))[^()]*)*\)\s*)/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.slice(1, -1)
.split(',')
.map(el => {
const param = el.split(' ');
@@ -682,7 +727,7 @@ export class MySQLClient extends AntaresCore {
return {
name: param[0] ? param[0].replaceAll('`', '') : '',
type: type[0],
length: +type[1]
length: +type[1] ? +type[1].replace(/\D/g, '') : ''
};
}).filter(el => el.name);
@@ -699,7 +744,7 @@ export class MySQLClient extends AntaresCore {
return {
definer: row['Create Function'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Function'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters,
parameters: parameters || [],
name: row.Function,
comment: row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Function'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Function'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
@@ -949,6 +994,25 @@ export class MySQLClient extends AntaresCore {
}, {});
}
async getProcesses () {
const sql = 'SELECT `ID`, `USER`, `HOST`, `DB`, `COMMAND`, `TIME`, `STATE`, LEFT(`INFO`, 51200) AS `INFO` FROM `information_schema`.`PROCESSLIST`';
const { rows } = await this.raw(sql);
return rows.map(row => {
return {
id: row.ID,
user: row.USER,
host: row.HOST,
db: row.DB,
command: row.COMMAND,
time: row.TIME,
state: row.STATE,
info: row.INFO
};
});
}
/**
* CREATE TABLE
*
@@ -1203,10 +1267,13 @@ export class MySQLClient extends AntaresCore {
for (const query of queries) {
if (!query) continue;
const timeStart = new Date();
let timeStop;
let keysArr = [];
const { rows, report, fields, keys } = await new Promise((resolve, reject) => {
const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => {
this._connection.query({ sql: query, nestTables }, async (err, response, fields) => {
timeStop = new Date();
const queryResult = response;
if (err)
@@ -1223,10 +1290,9 @@ export class MySQLClient extends AntaresCore {
name: field.orgName,
alias: field.name,
orgName: field.orgName,
schema: field.db,
schema: field.schema,
table: field.table,
tableAlias: field.table,
zerofill: field.zerofill,
orgTable: field.orgTable,
type: type.name,
length: type.length
@@ -1253,7 +1319,7 @@ export class MySQLClient extends AntaresCore {
const response = await this.getTableColumns(paramObj);
remappedFields = remappedFields.map(field => {
const detailedField = response.find(f => f.name === field.name);
if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema && detailedField.name === field.orgName)
if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema)
field = { ...detailedField, ...field };
return field;
});
@@ -1274,6 +1340,7 @@ export class MySQLClient extends AntaresCore {
}
resolve({
duration: timeStop - timeStart,
rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false,
report: !Array.isArray(queryResult) ? queryResult : false,
fields: remappedFields,
@@ -1283,7 +1350,7 @@ export class MySQLClient extends AntaresCore {
});
});
resultsArr.push({ rows, report, fields, keys });
resultsArr.push({ rows, report, fields, keys, duration });
}
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<template>
<div id="wrapper">
<div id="wrapper" :class="`theme-${applicationTheme}`">
<TheTitleBar />
<div id="window-content">
<TheSettingBar />
@@ -16,6 +16,7 @@
<TheFooter />
<TheNotificationsBoard />
<ModalNewConnection v-if="isNewConnModal" />
<TheScratchpad v-if="isScratchpad" />
<ModalSettings v-if="isSettingModal" />
<ModalDiscardChanges v-if="isUnsavedDiscardModal" />
</div>
@@ -24,7 +25,7 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { ipcRenderer } from 'electron';
import { ipcRenderer, remote } from 'electron';
export default {
name: 'App',
@@ -37,6 +38,7 @@ export default {
Workspace: () => import(/* webpackChunkName: "Workspace" */'@/components/Workspace'),
ModalNewConnection: () => import(/* webpackChunkName: "ModalNewConnection" */'@/components/ModalNewConnection'),
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings'),
TheScratchpad: () => import(/* webpackChunkName: "TheScratchpad" */'@/components/TheScratchpad'),
ModalDiscardChanges: () => import(/* webpackChunkName: "ModalDiscardChanges" */'@/components/ModalDiscardChanges')
},
data () {
@@ -48,12 +50,53 @@ export default {
isNewConnModal: 'application/isNewModal',
isEditModal: 'application/isEditModal',
isSettingModal: 'application/isSettingModal',
isScratchpad: 'application/isScratchpad',
connections: 'connections/getConnections',
applicationTheme: 'settings/getApplicationTheme',
isUnsavedDiscardModal: 'workspaces/isUnsavedDiscardModal'
})
},
mounted () {
ipcRenderer.send('check-for-updates');
const Menu = remote.Menu;
const InputMenu = Menu.buildFromTemplate([
{
label: this.$t('word.cut'),
role: 'cut'
},
{
label: this.$t('word.copy'),
role: 'copy'
},
{
label: this.$t('word.paste'),
role: 'paste'
},
{
type: 'separator'
},
{
label: this.$t('message.selectAll'),
role: 'selectall'
}
]);
document.body.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
let node = e.target;
while (node) {
if (node.nodeName.match(/^(input|textarea)$/i) || node.isContentEditable) {
InputMenu.popup(remote.getCurrentWindow());
break;
}
node = node.parentNode;
}
});
},
methods: {
...mapActions({

View File

@@ -24,7 +24,7 @@
<slot name="body" />
</div>
</div>
<div class="modal-footer">
<div v-if="!hideFooter" class="modal-footer">
<button
class="btn btn-primary mr-2"
@click.stop="confirmModal"
@@ -51,6 +51,10 @@ export default {
validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
default: 'small'
},
hideFooter: {
type: Boolean,
default: false
},
confirmText: String,
cancelText: String
},
@@ -74,6 +78,12 @@ export default {
else return '';
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
confirmModal () {
this.$emit('confirm');
@@ -82,6 +92,11 @@ export default {
hideModal () {
this.$emit('hide');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.hideModal();
}
}
};

View File

@@ -71,7 +71,6 @@ export default {
<style lang="scss">
.context {
display: flex;
color: $body-font-color;
font-size: 16px;
z-index: 400;
justify-content: center;
@@ -87,7 +86,6 @@ export default {
.context-container {
min-width: 100px;
z-index: 10;
box-shadow: 0 0 2px 0 #000;
padding: 0;
background: #1d1d1d;
border-radius: 0.1rem;
@@ -111,14 +109,10 @@ export default {
position: absolute;
left: 100%;
top: 0;
background: #1d1d1d;
box-shadow: 0 0 2px 0 #000;
min-width: 100px;
}
&:hover {
background: $primary-color;
.context-submenu {
display: block;
visibility: visible;

View File

@@ -0,0 +1,117 @@
<template>
<div class="editor-wrapper">
<div
:id="`editor-${id}`"
class="editor"
:class="editorClass"
:style="{height: `${height}px`}"
/>
</div>
</template>
<script>
import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import { mapGetters } from 'vuex';
export default {
name: 'BaseTextEditor',
props: {
value: String,
mode: { type: String, default: 'text' },
editorClass: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
showLineNumbers: { type: Boolean, default: true },
height: { type: Number, default: 200 }
},
data () {
return {
editor: null,
id: null
};
},
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
})
},
watch: {
mode () {
if (this.editor)
this.editor.session.setMode(`ace/mode/${this.mode}`);
},
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
autoComplete () {
if (this.editor) {
this.editor.setOptions({
enableLiveAutocompletion: this.autoComplete
});
}
},
lineWrap () {
if (this.editor) {
this.editor.setOptions({
wrap: this.lineWrap
});
}
}
},
created () {
this.id = this._uid;
},
mounted () {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value,
fontSize: '14px',
printMargin: false,
readOnly: this.readOnly,
showLineNumbers: this.showLineNumbers,
showGutter: this.showLineNumbers
});
this.editor.setOptions({
enableBasicAutocompletion: false,
wrap: this.lineWrap,
enableSnippets: false,
enableLiveAutocompletion: false
});
this.editor.session.on('change', () => {
const content = this.editor.getValue();
this.$emit('update:value', content);
});
if (this.autoFocus) {
setTimeout(() => {
this.editor.focus();
this.editor.resize();
}, 20);
}
setTimeout(() => {
this.editor.resize();
}, 20);
}
};
</script>
<style lang="scss" scoped>
.editor-wrapper {
.editor {
width: 100%;
}
}
.ace_.mdi {
display: inline-block;
width: 17px;
}
</style>

View File

@@ -63,13 +63,11 @@ export default {
<style lang="scss" scoped>
.file-uploader {
border: 0.05rem solid $bg-color-light;
border-radius: 0.1rem;
height: 1.8rem;
line-height: 1.2rem;
display: flex;
cursor: pointer;
background-color: $bg-color-gray;
transition: background 0.2s, border 0.2s, box-shadow 0.2s, color 0.2s;
position: relative;
flex: 1 1 auto;
@@ -80,8 +78,6 @@ export default {
.file-uploader-message {
display: flex;
border-right: 0.05rem solid $bg-color-light;
background-color: $bg-color;
}
.file-uploader-input {
@@ -105,7 +101,6 @@ export default {
:disabled {
.file-uploader {
cursor: not-allowed;
background-color: #151515;
opacity: 0.5;
}
}

View File

@@ -41,14 +41,16 @@ export default {
localScrollElement: null
};
},
watch: {
scrollElement () {
this.setScrollElement();
}
},
mounted () {
this._checkScrollPosition = this.checkScrollPosition.bind(this);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this._checkScrollPosition);
this.setScrollElement();
},
beforeDestroy () {
this.localScrollElement.removeEventListener('scroll', this._checkScrollPosition);
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
},
methods: {
checkScrollPosition (e) {
@@ -58,7 +60,7 @@ export default {
this.updateWindow(e);
}, 200);
},
updateWindow (e) {
updateWindow () {
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
const totalScrollHeight = this.items.length * this.itemHeight;
const offset = 50;
@@ -74,6 +76,14 @@ export default {
this.topHeight = firstCutIndex * this.itemHeight;
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
},
setScrollElement () {
if (this.localScrollElement)
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this.checkScrollPosition);
}
}
};

View File

@@ -90,7 +90,7 @@
</template>
<script>
import { mask } from 'vue-the-mask';
import { VueMaskDirective } from 'v-mask';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import BaseUploadInput from '@/components/BaseUploadInput';
import ForeignKeySelect from '@/components/ForeignKeySelect';
@@ -103,7 +103,7 @@ export default {
BaseUploadInput
},
directives: {
mask
mask: VueMaskDirective
},
props: {
type: String,
@@ -136,6 +136,8 @@ export default {
this.localType = 'datetime';
else if (TIME.includes(this.type))
this.localType = 'time';
else
this.localType = 'none';
return FakerMethods.getGroupsByType(this.localType);
},

View File

@@ -11,11 +11,11 @@
</option>
<option
v-for="row in foreignList"
:key="row.foreignColumn"
:value="row.foreignColumn"
:selected="row.foreignColumn === value"
:key="row.foreign_column"
:value="row.foreign_column"
:selected="row.foreign_column === value"
>
{{ row.foreignColumn }} {{ 'foreignDescription' in row ? ` - ${row.foreignDescription}` : '' | cutText }}
{{ row.foreign_column }} {{ 'foreign_description' in row ? ` - ${row.foreign_description}` : '' | cutText }}
</option>
</select>
</template>
@@ -51,11 +51,11 @@ export default {
}),
isValidDefault () {
if (!this.foreignList.length) return true;
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString());
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString());
}
},
async created () {
let firstTextField;
let foreignDesc;
const params = {
uid: this.selectedWorkspace,
schema: this.keyUsage.refSchema,
@@ -64,8 +64,10 @@ export default {
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success')
firstTextField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type)).name || false;
if (status === 'success') {
const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type));
foreignDesc = textField ? textField.name : false;
}
else
this.addNotification({ status: 'error', message: response });
}
@@ -77,7 +79,7 @@ export default {
const { status, response } = await Tables.getForeignList({
...params,
column: this.keyUsage.refField,
description: firstTextField
description: foreignDesc
});
if (status === 'success')

View File

@@ -0,0 +1,107 @@
<template>
<ConfirmModal
:confirm-text="$t('word.run')"
:cancel-text="$t('word.cancel')"
size="400"
@confirm="runRoutine"
@hide="closeModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-play mr-1" /> {{ $t('word.parameters') }}: {{ localRoutine.name }}
</div>
</template>
<div slot="body">
<div class="content">
<form class="form-horizontal">
<div
v-for="(parameter, i) in localRoutine.parameters"
:key="parameter._id"
class="form-group"
>
<div class="col-3">
<label class="form-label">{{ parameter.name }}</label>
</div>
<div class="col-9">
<div class="input-group">
<input
:ref="i === 0 ? 'firstInput' : ''"
v-model="values[parameter.name]"
class="form-input"
type="text"
>
<span class="input-group-addon field-type" :class="typeClass(parameter.type)">
{{ parameter.type }} {{ parameter.length | wrapNumber }}
</span>
</div>
</div>
</div>
</form>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalAskParameters',
components: {
ConfirmModal
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
localRoutine: Object
},
data () {
return {
values: {}
};
},
created () {
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput[0].focus();
}, 20);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr) => {
const value = isNaN(this.values[curr]) ? `"${this.values[curr]}"` : this.values[curr];
acc.push(value);
return acc;
}, []);
this.$emit('confirm', valArr);
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style scoped>
.field-type {
font-size: 0.6rem;
}
</style>

View File

@@ -59,15 +59,15 @@
<option value="maria">
MariaDB
</option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="pg">
PostgreSQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
Microsoft SQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
</select>
</div>
</div>
@@ -97,6 +97,18 @@
>
</div>
</div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
@@ -247,6 +259,7 @@
<script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast';
@@ -274,6 +287,11 @@ export default {
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () {
this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey);

View File

@@ -5,7 +5,7 @@
<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') }}
<i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editSchema') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -23,7 +23,7 @@
class="form-input"
type="text"
required
:placeholder="$t('message.databaseName')"
:placeholder="$t('message.schemaName')"
readonly
>
</div>
@@ -53,7 +53,7 @@
</div>
</div>
<div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="updateDatabase">
<button class="btn btn-primary mr-2" @click.stop="updateSchema">
{{ $t('word.update') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
@@ -66,10 +66,10 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database';
import Schema from '@/ipc-api/Schema';
export default {
name: 'ModalEditDatabase',
name: 'ModalEditSchema',
props: {
selectedDatabase: String
},
@@ -98,7 +98,7 @@ export default {
async created () {
let actualCollation;
try {
const { status, response } = await Database.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase });
const { status, response } = await Schema.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase });
if (status === 'success')
actualCollation = response;
@@ -130,10 +130,10 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async updateDatabase () {
async updateSchema () {
if (this.database.collation !== this.database.prevCollation) {
try {
const { status, response } = await Database.updateDatabase({
const { status, response } = await Schema.updateSchema({
uid: this.selectedWorkspace,
...this.database
});

View File

@@ -34,7 +34,7 @@
:field-obj="localRow[field.name]"
:value.sync="localRow[field.name]"
>
<span class="input-group-addon field-type" :class="`type-${field.type.toLowerCase()}`">
<span class="input-group-addon field-type" :class="typeClass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
@@ -184,8 +184,7 @@
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect';
@@ -195,9 +194,6 @@ export default {
components: {
FakerSelect
},
directives: {
mask
},
filters: {
wrapNumber (num) {
if (!num) return '';
@@ -262,6 +258,9 @@ export default {
if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
if (BIT.includes(field.type))
fieldDefault = field.default.replaceAll('\'', '').replaceAll('b', '');
if (DATETIME.includes(field.type)) {
if (field.default && field.default.toLowerCase().includes('current_timestamp')) {
let datePrecision = '';
@@ -287,6 +286,11 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification'
}),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () {
this.isInserting = true;
const rowToInsert = this.localRow;

View File

@@ -63,12 +63,12 @@
<option value="maria">
MariaDB
</option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql">
Microsoft SQL
</option>
<option value="pg">
PostgreSQL
</option>
<option value="oracledb">
Oracle DB
</option> -->
@@ -101,6 +101,18 @@
>
</div>
</div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
@@ -251,6 +263,7 @@
<script>
import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen';
import ModalAskCredentials from '@/components/ModalAskCredentials';
@@ -270,8 +283,9 @@ export default {
name: '',
client: 'mysql',
host: '127.0.0.1',
port: '3306',
user: 'root',
database: null,
port: null,
user: null,
password: '',
ask: false,
uid: uidGen('C'),
@@ -291,7 +305,13 @@ export default {
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () {
this.setDefaults();
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
@@ -307,20 +327,9 @@ export default {
addConnection: 'connections/addConnection'
}),
setDefaults () {
switch (this.connection.client) {
case 'mysql':
this.connection.port = '3306';
break;
case 'mssql':
this.connection.port = '1433';
break;
case 'pg':
this.connection.port = '5432';
break;
case 'oracledb':
this.connection.port = '1521';
break;
}
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
},
async startTest () {
this.isTesting = true;

View File

@@ -5,7 +5,7 @@
<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') }}
<i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewSchema') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -24,11 +24,11 @@
class="form-input"
type="text"
required
:placeholder="$t('message.databaseName')"
:placeholder="$t('message.schemaName')"
>
</div>
</div>
<div class="form-group">
<div v-if="customizations.collations" class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.collation') }}</label>
</div>
@@ -49,7 +49,11 @@
</div>
</div>
<div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="createDatabase">
<button
class="btn btn-primary mr-2"
:class="{'loading': isLoading}"
@click.stop="createSchema"
>
{{ $t('word.add') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
@@ -62,12 +66,13 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database';
import Schema from '@/ipc-api/Schema';
export default {
name: 'ModalNewDatabase',
name: 'ModalNewSchema',
data () {
return {
isLoading: false,
database: {
name: '',
collation: ''
@@ -83,8 +88,11 @@ export default {
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
},
customizations () {
return this.getWorkspace(this.selectedWorkspace).customizations;
},
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server') ? this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value : '';
}
},
created () {
@@ -101,9 +109,10 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async createDatabase () {
async createSchema () {
this.isLoading = true;
try {
const { status, response } = await Database.createDatabase({
const { status, response } = await Schema.createSchema({
uid: this.selectedWorkspace,
...this.database
});
@@ -118,6 +127,7 @@ export default {
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isLoading = false;
},
closeModal () {
this.$emit('close');

View File

@@ -25,7 +25,7 @@
>
</div>
</div>
<div class="form-group">
<div v-if="workspace.customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
@@ -37,7 +37,7 @@
>
</div>
</div>
<div class="form-group">
<div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4">
{{ $t('word.collation') }}
</label>
@@ -53,7 +53,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4">
{{ $t('word.engine') }}
</label>
@@ -103,10 +103,14 @@ export default {
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
if (this.workspace.customizations.collations)
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
return '';
},
defaultEngine () {
return this.workspace.engines.find(engine => engine.isDefault).name;
if (this.workspace.customizations.engines)
return this.workspace.engines.find(engine => engine.isDefault).name;
return '';
}
},
mounted () {

View File

@@ -69,7 +69,7 @@
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<span class="input-group-addon" :class="`type-${field.type.toLowerCase()}`">
<span class="input-group-addon" :class="typeCLass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
@@ -118,7 +118,7 @@
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { VueMaskDirective } from 'v-mask';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect';
@@ -129,7 +129,7 @@ export default {
ForeignKeySelect
},
directives: {
mask
mask: VueMaskDirective
},
filters: {
wrapNumber (num) {
@@ -222,6 +222,11 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification'
}),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () {
this.isInserting = true;
const rowToInsert = this.localRow;

View File

@@ -25,7 +25,7 @@
</div>
</div>
<div class="column col-6">
<div class="form-group">
<div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select v-model="localView.definer" class="form-select">
<option value="">
@@ -44,7 +44,7 @@
</div>
<div class="columns">
<div class="column col-4">
<div class="form-group">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
<input
@@ -67,7 +67,7 @@
</div>
</div>
<div class="column col-4">
<div class="form-group">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
<input
@@ -99,7 +99,7 @@
</div>
</div>
<div class="column col-4">
<div class="form-group">
<div v-if="workspace.customizations.viewUpdateOption" class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio">
<input

View File

@@ -0,0 +1,310 @@
<template>
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-memory mr-1" /> {{ $t('message.processesList') }}: {{ connectionName }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="processes-toolbar py-2 px-4">
<div class="dropdown">
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
:class="{'loading':isQuering}"
title="F5"
@click="getProcessesList"
>
<span>{{ $t('word.refresh') }}</span>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh ml-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h ml-1" />
</button>
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
<i class="mdi mdi-24px mdi-menu-down" />
</div>
<div class="menu px-3">
<span>{{ $t('word.autoRefresh') }}: <b>{{ +autorefreshTimer ? `${autorefreshTimer}s` : 'OFF' }}</b></span>
<input
v-model="autorefreshTimer"
class="slider no-border"
type="range"
min="0"
max="15"
step="0.5"
@change="setRefreshInterval"
>
</div>
</div>
</div>
<div class="workspace-query-info">
<div v-if="sortedResults.length">
{{ $t('word.processes') }}: <b>{{ sortedResults.length.toLocaleString() }}</b>
</div>
</div>
</div>
<div class="modal-body py-0 workspace-query-results">
<div
ref="tableWrapper"
class="vscroll"
:style="{'height': resultsSize+'px'}"
>
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr">
<div
v-for="(field, index) in fields"
:key="index"
class="th c-hand"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field)">
<span>{{ field.toUpperCase() }}</span>
<i
v-if="currentSort === field"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
</div>
</div>
</div>
</div>
</div>
<BaseVirtualScroll
ref="resultTable"
:items="sortedResults"
:item-height="22"
class="tbody"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<ProcessesListRow
v-for="row in items"
:key="row._id"
class="process-row"
:row="row"
@contextmenu="contextMenu"
@stop-refresh="stopRefresh"
/>
</template>
</BaseVirtualScroll>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import Schema from '@/ipc-api/Schema';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ProcessesListRow from '@/components/ProcessesListRow';
export default {
name: 'ModalProcessesList',
components: {
BaseVirtualScroll,
ProcessesListRow
},
props: {
connection: Object
},
data () {
return {
resultsSize: 1000,
isQuering: false,
autorefreshTimer: 0,
refreshInterval: null,
results: [],
fields: [],
currentSort: '',
currentSortDir: 'asc',
scrollElement: null
};
},
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName'
}),
connectionName () {
return this.getConnectionName(this.connection.uid);
},
sortedResults () {
if (this.currentSort) {
return [...this.results].sort((a, b) => {
let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
if (this.currentSortDir === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier;
return 0;
});
}
else
return this.results;
}
},
created () {
window.addEventListener('keydown', this.onKey, { capture: true });
},
updated () {
if (this.$refs.table)
this.refreshScroller();
if (this.$refs.tableWrapper)
this.scrollElement = this.$refs.tableWrapper;
},
mounted () {
this.getProcessesList();
window.addEventListener('resize', this.resizeResults);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async getProcessesList () {
this.isQuering = true;
// if table changes clear cached values
if (this.lastTable !== this.table)
this.results = [];
try { // Table data
const { status, response } = await Schema.getProcesses(this.connection.uid);
if (status === 'success') {
this.results = response;
this.fields = response.length ? Object.keys(response[0]) : [];
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
setRefreshInterval () {
this.clearRefresh();
if (+this.autorefreshTimer) {
this.refreshInterval = setInterval(() => {
if (!this.isQuering)
this.getProcessesList();
}, this.autorefreshTimer * 1000);
}
},
clearRefresh () {
if (this.refreshInterval)
clearInterval(this.refreshInterval);
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el) {
const size = el.offsetHeight;
this.resultsSize = size;
}
this.$refs.resultTable.updateWindow();
}
},
refreshScroller () {
this.resizeResults();
},
sort (field) {
if (field === this.currentSort) {
if (this.currentSortDir === 'asc')
this.currentSortDir = 'desc';
else
this.resetSort();
}
else {
this.currentSortDir = 'asc';
this.currentSort = field;
}
},
resetSort () {
this.currentSort = '';
this.currentSortDir = 'asc';
},
stopRefresh () {
this.autorefreshTimer = 0;
this.clearRefresh();
},
contextMenu () {},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
if (e.key === 'F5')
this.getProcessesList();
}
}
};
</script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.column-resizable {
&:hover,
&:active {
resize: horizontal;
overflow: hidden;
}
}
.table-column-title {
display: flex;
align-items: center;
}
.sort-icon {
font-size: 0.7rem;
line-height: 1;
margin-left: 0.2rem;
}
.result-tabs {
background: transparent !important;
margin: 0;
}
.modal {
align-items: flex-start;
.modal-container {
max-width: 75vw;
margin-top: 10vh;
.modal-body {
height: 80vh;
}
}
}
.processes-toolbar {
display: flex;
justify-content: space-between;
}
</style>

View File

@@ -30,6 +30,7 @@
<a class="c-hand">{{ $t('word.themes') }}</a>
</li>
<li
v-if="updateStatus !== 'disabled'"
class="tab-item"
:class="{'active': selectedTab === 'update'}"
@click="selectTab('update')"
@@ -139,20 +140,29 @@
<div class="column col-12 h6 text-uppercase mb-2">
{{ $t('message.applicationTheme') }}
</div>
<div class="column col-6 c-hand theme-block" :class="{'selected': applicationTheme === 'dark'}">
<div
class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'dark'}"
@click="changeApplicationTheme('dark')"
>
<img :src="require('@/images/dark.png').default" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name">
<div class="theme-name text-light">
<i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.dark') }}
</div>
</div>
</div>
<div class="column col-6 theme-block disabled" :class="{'selected': applicationTheme === 'light'}">
<div class="theme-name">
<div
class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'light'}"
@click="changeApplicationTheme('light')"
>
<img :src="require('@/images/light.png').default" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-dark">
<i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.light') }} (Coming)
{{ $t('word.light') }}
</div>
</div>
</div>
@@ -185,8 +195,9 @@
</select>
</div>
<div class="column col-12">
<QueryEditor
<BaseTextEditor
:value="exampleQuery"
mode="sql"
:workspace="workspace"
:read-only="true"
:height="270"
@@ -222,14 +233,14 @@
import { mapActions, mapGetters } from 'vuex';
import localesNames from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate';
import QueryEditor from '@/components/QueryEditor';
import BaseTextEditor from '@/components/BaseTextEditor';
const { shell } = require('electron');
export default {
name: 'ModalSettings',
components: {
ModalSettingsUpdate,
QueryEditor
BaseTextEditor
},
data () {
return {
@@ -350,6 +361,7 @@ ORDER BY
changeLocale: 'settings/changeLocale',
changeAutoComplete: 'settings/changeAutoComplete',
changeLineWrap: 'settings/changeLineWrap',
changeApplicationTheme: 'settings/changeApplicationTheme',
changeEditorTheme: 'settings/changeEditorTheme',
updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
}),
@@ -413,7 +425,6 @@ ORDER BY
top: 0;
height: 100%;
width: 100%;
text-shadow: 0 0 8px #000;
}
}
}

View File

@@ -36,11 +36,17 @@
{{ $t('message.restartToInstall') }}
</button>
</div>
<div class="form-group mt-4">
<label class="form-switch d-inline-block disabled" @click.prevent="toggleAllowPrerelease">
<input type="checkbox" :checked="allowPrerelease">
<i class="form-icon" /> {{ $t('message.includeBetaUpdates') }}
</label>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import { ipcRenderer } from 'electron';
export default {
@@ -48,7 +54,8 @@ export default {
computed: {
...mapGetters({
updateStatus: 'application/getUpdateStatus',
downloadPercentage: 'application/getDownloadProgress'
downloadPercentage: 'application/getDownloadProgress',
allowPrerelease: 'settings/getAllowPrerelease'
}),
updateMessage () {
switch (this.updateStatus) {
@@ -70,18 +77,18 @@ export default {
}
},
methods: {
...mapActions({
changeAllowPrerelease: 'settings/changeAllowPrerelease'
}),
checkForUpdates () {
ipcRenderer.send('check-for-updates');
},
restartToUpdate () {
ipcRenderer.send('restart-to-update');
},
toggleAllowPrerelease () {
this.changeAllowPrerelease(!this.allowPrerelease);
}
}
};
</script>
<style lang="scss">
.empty {
color: $body-font-color;
}
</style>

View File

@@ -0,0 +1,155 @@
<template>
<div class="tr" @click="selectRow($event, row._id)">
<div
v-for="(col, cKey) in row"
v-show="cKey !== '_id'"
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
</template>
</div>
<ConfirmModal
v-if="isInfoModal"
:confirm-text="$t('word.update')"
:cancel-text="$t('word.close')"
size="medium"
:hide-footer="true"
@hide="hideInfoModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-information-outline mr-1" /> {{ $t('message.processInfo') }}
</div>
</template>
<div :slot="'body'">
<div>
<div>
<TextEditor
:value="row.info || ''"
editor-class="textarea-editor"
:mode="editorMode"
:read-only="true"
/>
</div>
</div>
</div>
</ConfirmModal>
</div>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
export default {
name: 'ProcessesListRow',
components: {
ConfirmModal,
TextEditor
},
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
}
},
props: {
row: Object
},
data () {
return {
isInlineEditor: {},
isInfoModal: false,
editorMode: 'sql'
};
},
computed: {},
watch: {
fields () {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
},
methods: {
isNull (value) {
return value === null ? ' is-null' : '';
},
selectRow (event, row) {
this.$emit('select-row', event, row);
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
},
hideInfoModal () {
this.isInfoModal = false;
},
dblClick (col) {
if (col !== 'info') return;
this.$emit('stop-refresh');
this.isInfoModal = true;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
}
}
};
</script>
<style lang="scss">
.editable-field {
margin: 0;
border: none;
line-height: 1;
width: 100%;
position: absolute;
left: 0;
right: 0;
}
.cell-content {
display: block;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.textarea-editor {
height: 50vh !important;
}
.editor-field-info {
margin-top: 0.4rem;
display: flex;
justify-content: space-between;
white-space: normal;
}
.editor-buttons {
display: flex;
justify-content: space-evenly;
.btn {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="editor-wrapper">
<div
ref="editor"
:id="`editor-${id}`"
class="editor"
:style="{height: `${height}px`}"
/>
@@ -12,7 +12,7 @@
import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools';
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
export default {
@@ -20,6 +20,7 @@ export default {
props: {
value: String,
workspace: Object,
isSelected: Boolean,
schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
@@ -29,14 +30,17 @@ export default {
return {
editor: null,
fields: [],
baseCompleter: []
customCompleter: [],
id: null,
lastSchema: null
};
},
computed: {
...mapGetters({
editorTheme: 'settings/getEditorTheme',
autoComplete: 'settings/getAutoComplete',
lineWrap: 'settings/getLineWrap'
lineWrap: 'settings/getLineWrap',
baseCompleter: 'application/getBaseCompleter'
}),
tables () {
return this.workspace
@@ -163,10 +167,24 @@ export default {
wrap: this.lineWrap
});
}
},
isSelected () {
if (this.isSelected)
this.lastSchema = this.schema;
},
lastSchema () {
if (this.editor) {
this.editor.completers = this.baseCompleter.map(el => Object.assign({}, el));
this.setCustomCompleter();
}
}
},
created () {
this.id = this._uid;
this.lastSchema = this.schema;
},
mounted () {
this.editor = ace.edit(this.$refs.editor, {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.value,
@@ -182,26 +200,10 @@ export default {
enableLiveAutocompletion: this.autoComplete
});
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
if (!this.baseCompleter.length)
this.setBaseCompleters(this.editor.completers.map(el => Object.assign({}, el)));
this.baseCompleter = this.editor.completers;
this.setCustomCompleter();
this.editor.commands.on('afterExec', e => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
@@ -224,13 +226,13 @@ export default {
}).catch(console.log);
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
else
this.editor.completers = this.baseCompleter;
this.editor.completers = this.customCompleter;
}
});
@@ -239,6 +241,22 @@ export default {
this.$emit('update:value', content);
});
this.editor.on('guttermousedown', e => {
const target = e.domEvent.target;
if (target.className.indexOf('ace_gutter-cell') === -1)
return;
if (e.clientX > 25 + target.getBoundingClientRect().left)
return;
const row = e.getDocumentPosition().row;
const breakpoints = e.editor.session.getBreakpoints(row, 0);
if (typeof breakpoints[row] === typeof undefined)
e.editor.session.setBreakpoint(row);
else
e.editor.session.clearBreakpoint(row);
e.stop();
});
if (this.autoFocus) {
setTimeout(() => {
this.editor.focus();
@@ -249,6 +267,33 @@ export default {
setTimeout(() => {
this.editor.resize();
}, 20);
},
methods: {
...mapActions({
setBaseCompleters: 'application/setBaseCompleter'
}),
setCustomCompleter () {
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
this.customCompleter = this.editor.completers;
}
}
};
</script>
@@ -267,16 +312,20 @@ export default {
width: 17px;
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
background-color: #c9561a99;
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover {
background-color: #c9571a33;
border: none;
}
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight {
color: #e0d00c;
.ace_gutter-cell.ace_breakpoint {
&::before {
content: '\F0403';
position: absolute;
left: 3px;
top: 2px;
color: $primary-color;
display: inline-block;
font: normal normal normal 24px/1 "Material Design Icons", sans-serif;
font-size: inherit;
text-rendering: auto;
line-height: inherit;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
</style>

View File

@@ -64,13 +64,11 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
background: $primary-color;
padding: 0 0.2rem;
position: fixed;
bottom: 0;
left: 0;
right: 0;
box-shadow: 0 0 1px 0 #000;
.footer-elements {
list-style: none;
@@ -88,10 +86,6 @@ export default {
&.footer-link {
cursor: pointer;
transition: background 0.2s;
&:hover {
background: rgba($color: #fff, $alpha: 0.1);
}
}
}
}

View File

@@ -0,0 +1,75 @@
<template>
<ConfirmModal
:confirm-text="$t('word.update')"
:cancel-text="$t('word.close')"
size="large"
:hide-footer="true"
@hide="hideScratchpad"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-notebook-edit-outline mr-1" /> {{ $t('word.scratchpad') }}
</div>
</template>
<div :slot="'body'">
<div>
<div>
<TextEditor
:value.sync="localNotes"
editor-class="textarea-editor"
mode="markdown"
:auto-focus="true"
:show-line-numbers="false"
/>
</div>
<small class="text-gray">{{ $t('message.markdownSupported') }}</small>
</div>
</div>
</ConfirmModal>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
export default {
name: 'TheScratchpad',
components: {
ConfirmModal,
TextEditor
},
data () {
return {
localNotes: '',
debounceTimeout: null
};
},
computed: {
...mapGetters({
notes: 'scratchpad/getNotes'
})
},
watch: {
localNotes () {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => {
this.changeNotes(this.localNotes);
}, 200);
}
},
created () {
this.localNotes = this.notes;
},
methods: {
...mapActions({
hideScratchpad: 'application/hideScratchpad',
changeNotes: 'scratchpad/changeNotes'
}),
hideModal () {
this.$emit('hide');
}
}
};
</script>

View File

@@ -36,6 +36,10 @@
<div class="settingbar-bottom-elements">
<ul class="settingbar-elements">
<li class="settingbar-element btn btn-link ex-tooltip" @click="showScratchpad">
<i class="settingbar-element-icon mdi mdi-24px mdi-notebook-edit-outline text-light" />
<span class="ex-tooltip-content">{{ $t('word.scratchpad') }}</span>
</li>
<li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')">
<i class="settingbar-element-icon mdi mdi-24px mdi-cog text-light" :class="{' badge badge-update': hasUpdates}" />
<span class="ex-tooltip-content">{{ $t('word.settings') }}</span>
@@ -90,6 +94,7 @@ export default {
updateConnections: 'connections/updateConnections',
showNewConnModal: 'application/showNewConnModal',
showSettingModal: 'application/showSettingModal',
showScratchpad: 'application/showScratchpad',
selectWorkspace: 'workspaces/selectWorkspace'
}),
contextMenu (event, connection) {
@@ -117,9 +122,7 @@ export default {
flex-direction: column;
justify-content: space-between;
align-items: center;
background: $bg-color-light;
padding: 0;
box-shadow: 0 0 1px 0 #000;
z-index: 9;
.settingbar-top-elements {
@@ -134,7 +137,6 @@ export default {
.settingbar-bottom-elements {
padding-top: 0.5rem;
background: $bg-color-light;
z-index: 1;
}
@@ -162,7 +164,6 @@ export default {
}
&.selected {
border-left-color: $body-font-color;
opacity: 1;
}
@@ -171,12 +172,10 @@ export default {
bottom: -10px;
right: 0;
position: absolute;
background: $success-color;
}
&.badge-update::after {
bottom: initial;
background: $primary-color;
}
}
}

View File

@@ -102,12 +102,10 @@ export default {
display: flex;
position: relative;
justify-content: space-between;
background: $bg-color-light;
align-items: center;
height: $titlebar-height;
-webkit-app-region: drag;
user-select: none;
box-shadow: 0 0 1px 0 #000;
z-index: 9999;
.titlebar-resizer {
@@ -149,11 +147,6 @@ export default {
&:hover {
opacity: 1;
background: rgba($color: #fff, $alpha: 0.2);
}
&.close-button:hover {
background: red;
}
}
}

View File

@@ -7,19 +7,51 @@
ref="tabWrap"
class="tab tab-block column col-12"
>
<li class="tab-item d-none">
<a class="tab-link workspace-tools-link">
<li class="tab-item dropdown tools-dropdown">
<a
class="tab-link workspace-tools-link dropdown-toggle"
tabindex="0"
:title="$t('word.tools')"
>
<i class="mdi mdi-24px mdi-tools" />
</a>
<ul class="menu text-left text-uppercase">
<li v-if="workspace.customizations.processesList" class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.variables"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span>
</a>
</li>
<li
v-if="workspace.customizations.usersManagement"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span>
</a>
</li>
</ul>
</li>
<li
v-if="schemaChild"
v-if="schemaChild && isSettingSupported"
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" />
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="schemaChild">{{ $t('word.settings').toUpperCase() }}: {{ schemaChild }}</span>
</a>
</li>
@@ -109,11 +141,16 @@
<WorkspaceQueryTab
v-for="tab of queryTabs"
:key="tab.uid"
:tab-uid="tab.uid"
:tab="tab"
:is-selected="selectedTab === tab.uid"
:connection="connection"
/>
</div>
<ModalProcessesList
v-if="isProcessesModal"
:connection="connection"
@close="hideProcessesModal"
/>
</div>
</template>
@@ -129,6 +166,7 @@ import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
import ModalProcessesList from '@/components/ModalProcessesList';
export default {
name: 'Workspace',
@@ -141,14 +179,16 @@ export default {
WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabScheduler
WorkspacePropsTabScheduler,
ModalProcessesList
},
props: {
connection: Object
},
data () {
return {
hasWheelEvent: false
hasWheelEvent: false,
isProcessesModal: false
};
},
computed: {
@@ -162,15 +202,31 @@ export default {
isSelected () {
return this.selectedWorkspace === this.connection.uid;
},
isSettingSupported () {
if (this.workspace.breadcrumbs.table && this.workspace.customizations.tableSettings) return true;
if (this.workspace.breadcrumbs.view && this.workspace.customizations.viewSettings) return true;
if (this.workspace.breadcrumbs.trigger && this.workspace.customizations.triggerSettings) return true;
if (this.workspace.breadcrumbs.procedure && this.workspace.customizations.routineSettings) return true;
if (this.workspace.breadcrumbs.function && this.workspace.customizations.functionSettings) return true;
if (this.workspace.breadcrumbs.scheduler && this.workspace.customizations.schedulerSettings) return true;
return false;
},
selectedTab () {
if (
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
) ||
(
this.workspace.breadcrumbs.table === null &&
this.workspace.breadcrumbs.view === null &&
this.workspace.selected_tab === 'data'
)
)
return this.queryTabs[0].uid;
@@ -214,7 +270,7 @@ export default {
removeTab: 'workspaces/removeTab'
}),
addTab () {
this.newTab(this.connection.uid);
this.newTab({ uid: this.connection.uid });
if (!this.hasWheelEvent) {
this.$refs.tabWrap.addEventListener('wheel', e => {
@@ -227,6 +283,12 @@ export default {
closeTab (tUid) {
if (this.queryTabs.length === 1) return;
this.removeTab({ uid: this.connection.uid, tab: tUid });
},
showProcessesModal () {
this.isProcessesModal = true;
},
hideProcessesModal () {
this.isProcessesModal = false;
}
}
};
@@ -242,7 +304,6 @@ export default {
height: calc(100vh - #{$excluding-size});
.tab-block {
background: $bg-color-light;
margin-top: 0;
flex-direction: row;
align-items: flex-start;
@@ -262,7 +323,6 @@ export default {
> a {
padding: 0.2rem 0.8rem;
color: $body-font-color;
cursor: pointer;
display: flex;
align-items: center;
@@ -291,6 +351,42 @@ export default {
opacity: 1;
}
&.tools-dropdown {
.tab-link:focus {
opacity: 1;
outline: 0;
box-shadow: none;
}
.menu {
min-width: 100%;
.menu-item a {
border-radius: 0.1rem;
color: inherit;
display: block;
margin: 0 -0.4rem;
padding: 0.2rem 0.4rem;
text-decoration: none;
white-space: nowrap;
border: 0;
.tool-icon {
line-height: 1;
display: inline-block;
font-size: 20px;
}
}
}
z-index: 9;
position: absolute;
}
&.tools-dropdown + .tab-item {
margin-left: 56px;
}
.workspace-tools-link {
padding-bottom: 0;
padding-top: 0.3rem;
@@ -310,11 +406,9 @@ export default {
.th {
position: sticky;
top: 0;
background: $bg-color;
border: 1px solid;
border-left: none;
border-bottom-width: 2px;
border-color: $bg-color-light;
padding: 0;
font-weight: 700;
font-size: 0.7rem;
@@ -329,7 +423,6 @@ export default {
.td {
border-right: 1px solid;
border-bottom: 1px solid;
border-color: $bg-color-light;
padding: 0 0.4rem;
text-overflow: ellipsis;
max-width: 200px;
@@ -339,8 +432,6 @@ export default {
position: relative;
&:focus {
box-shadow: inset 0 0 0 1px $body-font-color;
background: rgba($color: #000, $alpha: 0.3);
outline: none;
}
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="columns">
<div class="column col-12 empty text-light">
<div class="column col-12 empty">
<div class="empty-icon">
<i class="mdi mdi-48px mdi-power-plug-off" />
</div>

View File

@@ -11,7 +11,7 @@
<span v-if="workspace.connected" class="workspace-explorebar-tools">
<i
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewDatabase')"
:title="$t('message.createNewSchema')"
@click="showNewDBModal"
/>
<i
@@ -27,24 +27,35 @@
/>
</span>
</div>
<div class="workspace-explorebar-search">
<div v-if="workspace.connected" class="has-icon-right">
<input
v-model="searchTerm"
class="form-input input-sm"
type="text"
:placeholder="$t('message.searchForElements')"
>
<i class="form-icon mdi mdi-magnify mdi-18px" />
</div>
</div>
<WorkspaceConnectPanel
v-if="!workspace.connected"
class="workspace-explorebar-body"
:connection="connection"
/>
<div v-else class="workspace-explorebar-body">
<WorkspaceExploreBarDatabase
<WorkspaceExploreBarSchema
v-for="db of workspace.structure"
:key="db.name"
:database="db"
:connection="connection"
@show-database-context="openDatabaseContext"
@show-schema-context="openSchemaContext"
@show-table-context="openTableContext"
@show-misc-context="openMiscContext"
/>
</div>
</div>
<ModalNewDatabase
<ModalNewSchema
v-if="isNewDBModal"
@close="hideNewDBModal"
@reload="refresh"
@@ -117,7 +128,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import _ from 'lodash';// TODO: remove
import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
@@ -127,11 +137,11 @@ import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase';
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext';
import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema';
import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext';
import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import ModalNewDatabase from '@/components/ModalNewDatabase';
import ModalNewSchema from '@/components/ModalNewSchema';
import ModalNewTable from '@/components/ModalNewTable';
import ModalNewView from '@/components/ModalNewView';
import ModalNewTrigger from '@/components/ModalNewTrigger';
@@ -143,11 +153,11 @@ export default {
name: 'WorkspaceExploreBar',
components: {
WorkspaceConnectPanel,
WorkspaceExploreBarDatabase,
WorkspaceExploreBarSchema,
DatabaseContext,
TableContext,
MiscContext,
ModalNewDatabase,
ModalNewSchema,
ModalNewTable,
ModalNewView,
ModalNewTrigger,
@@ -172,6 +182,8 @@ export default {
isNewSchedulerModal: false,
localWidth: null,
explorebarWidthInterval: null,
searchTermInterval: null,
isDatabaseContext: false,
isTableContext: false,
isMiscContext: false,
@@ -182,7 +194,8 @@ export default {
selectedDatabase: '',
selectedTable: null,
selectedMisc: null
selectedMisc: null,
searchTerm: ''
};
},
computed: {
@@ -199,11 +212,22 @@ export default {
}
},
watch: {
localWidth: _.debounce(function (val) {
this.changeExplorebarSize(val);
}, 500),
localWidth (val) {
clearTimeout(this.explorebarWidthInterval);
this.explorebarWidthInterval = setTimeout(() => {
this.changeExplorebarSize(val);
}, 500);
},
isSelected (val) {
if (val) this.localWidth = this.explorebarSize;
},
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => {
this.setSearchTerm(this.searchTerm);
}, 200);
}
},
created () {
@@ -225,6 +249,7 @@ export default {
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
selectTab: 'workspaces/selectTab',
setSearchTerm: 'workspaces/setSearchTerm',
addNotification: 'notifications/addNotification',
changeExplorebarSize: 'settings/changeExplorebarSize'
}),
@@ -274,8 +299,8 @@ export default {
else
this.addNotification({ status: 'error', message: response });
},
openDatabaseContext (payload) {
this.selectedDatabase = payload.database;
openSchemaContext (payload) {
this.selectedDatabase = payload.schema;
this.databaseContextEvent = payload.event;
this.isDatabaseContext = true;
},
@@ -435,8 +460,6 @@ export default {
justify-content: flex-start;
align-items: center;
text-align: left;
background: $bg-color-gray;
box-shadow: 0 0 1px 0 #000;
z-index: 8;
flex: initial;
position: relative;
@@ -477,9 +500,40 @@ export default {
}
}
.workspace-explorebar-search {
width: 100%;
display: flex;
justify-content: space-between;
font-size: 0.6rem;
height: 28px;
.has-icon-right {
width: 100%;
padding: 0.1rem;
.form-icon {
opacity: 0.5;
transition: opacity 0.2s;
}
.form-input {
height: 1.2rem;
padding-left: 0.2rem;
&:focus + .form-icon {
opacity: 0.9;
}
&::placeholder {
opacity: 0.6;
}
}
}
}
.workspace-explorebar-body {
width: 100%;
height: calc((100vh - 30px) - #{$excluding-size});
height: calc((100vh - 58px) - #{$excluding-size});
overflow: overlay;
padding: 0 0.1rem;
}

View File

@@ -5,8 +5,8 @@
>
<div
v-if="['procedure', 'function'].includes(selectedMisc.type)"
class="context-element disabled"
@click="showRunModal"
class="context-element"
@click="runElementCheck"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-play text-light pr-1" /> {{ $t('word.run') }}</span>
</div>
@@ -29,6 +29,12 @@
</div>
</div>
</ConfirmModal>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localElement"
@confirm="runElement"
@close="hideAskParamsModal"
/>
</BaseContextMenu>
</template>
@@ -36,6 +42,7 @@
import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions';
@@ -45,7 +52,8 @@ export default {
name: 'WorkspaceExploreBarMiscContext',
components: {
BaseContextMenu,
ConfirmModal
ConfirmModal,
ModalAskParameters
},
props: {
contextEvent: MouseEvent,
@@ -54,7 +62,9 @@ export default {
data () {
return {
isDeleteModal: false,
isRunModal: false
isRunModal: false,
isAskingParameters: false,
localElement: {}
};
},
computed: {
@@ -83,7 +93,8 @@ export default {
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
@@ -94,11 +105,12 @@ export default {
hideDeleteModal () {
this.isDeleteModal = false;
},
showRunModal () {
this.isRunModal = true;
showAskParamsModal () {
this.isAskingParameters = true;
},
hideRunModal () {
this.isRunModal = false;
hideAskParamsModal () {
this.isAskingParameters = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
@@ -148,6 +160,105 @@ export default {
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
runElementCheck () {
if (this.selectedMisc.type === 'procedure')
this.runRoutineCheck();
else if (this.selectedMisc.type === 'function')
this.runFunctionCheck();
},
runElement (params) {
if (this.selectedMisc.type === 'procedure')
this.runRoutine(params);
else if (this.selectedMisc.type === 'function')
this.runFunction(params);
},
async runRoutineCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
routine: this.workspace.breadcrumbs.procedure
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success')
this.localElement = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length)
this.showAskParamsModal();
else
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql;
switch (this.workspace.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.closeContext();
},
async runFunctionCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
func: this.workspace.breadcrumbs.function
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success')
this.localElement = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql;
switch (this.workspace.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, autorun: true });
this.closeContext();
}
}
};

View File

@@ -4,7 +4,7 @@
class="accordion-header database-name"
:class="{'text-bold': breadcrumbs.schema === database.name}"
@click="selectSchema(database.name)"
@contextmenu.prevent="showDatabaseContext($event, database.name)"
@contextmenu.prevent="showSchemaContext($event, database.name)"
>
<div v-if="isLoading" class="icon loading" />
<i v-else class="icon mdi mdi-18px mdi-chevron-right" />
@@ -15,7 +15,7 @@
<div class="database-tables">
<ul class="menu menu-nav pt-0">
<li
v-for="table of database.tables"
v-for="table of filteredTables"
:key="table.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@@ -24,7 +24,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span>{{ table.name }}</span>
<span v-html="highlightWord(table.name)" />
</a>
<div
v-if="table.type === 'table'"
@@ -37,7 +37,7 @@
</ul>
</div>
<div v-if="database.triggers.length" class="database-misc">
<div v-if="filteredTriggers.length && customizations.triggers" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}">
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
@@ -47,7 +47,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="trigger of database.triggers"
v-for="trigger of filteredTriggers"
:key="trigger.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@@ -56,7 +56,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-table-cog mdi-18px mr-1" />
<span>{{ trigger.name }}</span>
<span v-html="highlightWord(trigger.name)" />
</a>
</li>
</ul>
@@ -65,7 +65,7 @@
</details>
</div>
<div v-if="database.procedures.length" class="database-misc">
<div v-if="filteredProcedures.length && customizations.routines" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}">
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
@@ -75,7 +75,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="procedure of database.procedures"
v-for="procedure of filteredProcedures"
:key="procedure.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure === procedure.name}"
@@ -84,7 +84,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span>{{ procedure.name }}</span>
<span v-html="highlightWord(procedure.name)" />
</a>
</li>
</ul>
@@ -93,7 +93,7 @@
</details>
</div>
<div v-if="database.functions.length" class="database-misc">
<div v-if="filteredFunctions.length && customizations.functions" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}">
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
@@ -103,7 +103,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="func of database.functions"
v-for="func of filteredFunctions"
:key="func.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@@ -112,7 +112,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" />
<span>{{ func.name }}</span>
<span v-html="highlightWord(func.name)" />
</a>
</li>
</ul>
@@ -121,7 +121,7 @@
</details>
</div>
<div v-if="database.schedulers.length" class="database-misc">
<div v-if="filteredSchedulers.length && customizations.schedulers" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}">
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
@@ -131,7 +131,7 @@
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="scheduler of database.schedulers"
v-for="scheduler of filteredSchedulers"
:key="scheduler.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@@ -140,7 +140,7 @@
>
<a class="table-name">
<i class="table-icon mdi mdi-calendar-clock mdi-18px mr-1" />
<span>{{ scheduler.name }}</span>
<span v-html="highlightWord(scheduler.name)" />
</a>
</li>
</ul>
@@ -157,7 +157,7 @@ import { mapActions, mapGetters } from 'vuex';
import { formatBytes } from 'common/libs/formatBytes';
export default {
name: 'WorkspaceExploreBarDatabase',
name: 'WorkspaceExploreBarSchema',
props: {
database: Object,
connection: Object
@@ -170,11 +170,34 @@ export default {
computed: {
...mapGetters({
getLoadedSchemas: 'workspaces/getLoadedSchemas',
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
getSearchTerm: 'workspaces/getSearchTerm',
applicationTheme: 'settings/getApplicationTheme'
}),
searchTerm () {
return this.getSearchTerm(this.connection.uid);
},
filteredTables () {
return this.database.tables.filter(table => table.name.search(this.searchTerm) >= 0);
},
filteredTriggers () {
return this.database.triggers.filter(trigger => trigger.name.search(this.searchTerm) >= 0);
},
filteredProcedures () {
return this.database.procedures.filter(procedure => procedure.name.search(this.searchTerm) >= 0);
},
filteredFunctions () {
return this.database.functions.filter(func => func.name.search(this.searchTerm) >= 0);
},
filteredSchedulers () {
return this.database.schedulers.filter(scheduler => scheduler.name.search(this.searchTerm) >= 0);
},
breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs;
},
customizations () {
return this.getWorkspace(this.connection.uid).customizations;
},
loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid);
},
@@ -203,9 +226,9 @@ export default {
this.changeBreadcrumbs({ schema, table: null });
},
showDatabaseContext (event, database) {
this.changeBreadcrumbs({ schema: database, table: null });
this.$emit('show-database-context', { event, database });
showSchemaContext (event, schema) {
this.selectSchema(schema);
this.$emit('show-schema-context', { event, schema });
},
showTableContext (event, table) {
this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name });
@@ -217,11 +240,22 @@ export default {
},
piePercentage (val) {
const perc = val / this.maxSize * 100;
return { background: `conic-gradient(lime ${perc}%, white 0)` };
if (this.applicationTheme === 'dark')
return { background: `conic-gradient(lime ${perc}%, white 0)` };
else
return { background: `conic-gradient(teal ${perc}%, silver 0)` };
},
setBreadcrumbs (payload) {
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
this.changeBreadcrumbs(payload);
},
highlightWord (string) {
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary">$1</span>');
}
else
return string;
}
}
};
@@ -229,6 +263,12 @@ export default {
<style lang="scss">
.workspace-explorebar-database {
.database-name {
position: sticky;
top: 0;
z-index: 2;
}
.database-name,
.misc-name,
a.table-name {
@@ -271,26 +311,15 @@ export default {
.database-name,
.misc-name {
&:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px;
}
}
a.table-name {
&:hover {
color: inherit;
background: inherit;
}
}
.menu-item {
line-height: 1.2;
position: relative;
&:hover {
color: $body-font-color;
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px;
}
}

View File

@@ -7,27 +7,55 @@
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div class="context-element" @click="showCreateTableModal">
<div
v-if="workspace.customizations.tableAdd"
class="context-element"
@click="showCreateTableModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div>
<div class="context-element" @click="showCreateViewModal">
<div
v-if="workspace.customizations.viewAdd"
class="context-element"
@click="showCreateViewModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
</div>
<div class="context-element" @click="showCreateTriggerModal">
<div
v-if="workspace.customizations.triggerAdd"
class="context-element"
@click="showCreateTriggerModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div>
<div class="context-element" @click="showCreateRoutineModal">
<div
v-if="workspace.customizations.routineAdd"
class="context-element"
@click="showCreateRoutineModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div>
<div class="context-element" @click="showCreateFunctionModal">
<div
v-if="workspace.customizations.functionAdd"
class="context-element"
@click="showCreateFunctionModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div>
<div class="context-element" @click="showCreateSchedulerModal">
<div
v-if="workspace.customizations.schedulerAdd"
class="context-element"
@click="showCreateSchedulerModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div>
</div>
</div>
<div class="context-element" @click="showEditModal">
<div
v-if="workspace.customizations.schemaEdit"
class="context-element"
@click="showEditModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div>
<div class="context-element" @click="showDeleteModal">
@@ -36,12 +64,12 @@
<ConfirmModal
v-if="isDeleteModal"
@confirm="deleteDatabase"
@confirm="deleteSchema"
@hide="hideDeleteModal"
>
<template slot="header">
<div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteDatabase') }}
<i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteSchema') }}
</div>
</template>
<div slot="body">
@@ -50,7 +78,7 @@
</div>
</div>
</ConfirmModal>
<ModalEditDatabase
<ModalEditSchema
v-if="isEditModal"
:selected-database="selectedDatabase"
@close="hideEditModal"
@@ -62,15 +90,15 @@
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';
import ModalEditSchema from '@/components/ModalEditSchema';
import Schema from '@/ipc-api/Schema';
export default {
name: 'WorkspaceExploreBarDatabaseContext',
name: 'WorkspaceExploreBarSchemaContext',
components: {
BaseContextMenu,
ConfirmModal,
ModalEditDatabase
ModalEditSchema
},
props: {
contextEvent: MouseEvent,
@@ -130,9 +158,9 @@ export default {
closeContext () {
this.$emit('close-context');
},
async deleteDatabase () {
async deleteSchema () {
try {
const { status, response } = await Database.deleteDatabase({
const { status, response } = await Schema.deleteSchema({
uid: this.selectedWorkspace,
database: this.selectedDatabase
});

View File

@@ -2,6 +2,7 @@
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
class="options-modal"
@confirm="confirmForeignsChange"
@hide="$emit('hide')"
>
@@ -36,7 +37,7 @@
v-for="foreign in foreignProxy"
:key="foreign._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-foreign': selectedForeignID === foreign._id}"
:class="{'selected-element': selectedForeignID === foreign._id}"
@click="selectForeign($event, foreign._id)"
>
<div class="tile-icon">
@@ -268,8 +269,10 @@ export default {
this.$emit('foreigns-update', this.foreignProxy);
},
selectForeign (event, id) {
if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field'))
if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field')) {
this.selectedForeignID = id;
this.getRefFields();
}
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
@@ -369,23 +372,16 @@ export default {
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-foreign {
background: $bg-color-light;
&.selected-element {
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 80px;
overflow: auto;

View File

@@ -2,7 +2,8 @@
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmIndexesChange"
class="options-modal"
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
@@ -34,14 +35,14 @@
<div ref="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param.name"
:key="param._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}"
@click="selectParameter($event, param.name)"
:class="{'selected-element': selectedParam === param._id}"
@click="selectParameter($event, param._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" />
<i class="mdi mdi-hexagon mdi-24px" :class="typeClass(param.type)" />
</div>
</div>
<div class="tile-content">
@@ -54,7 +55,7 @@
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeParameter(param.name)"
@click.prevent="removeParameter(param._id)"
>
<i class="mdi mdi-close" />
</button>
@@ -139,6 +140,7 @@
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
@@ -162,7 +164,7 @@ export default {
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam);
return this.parametersProxy.find(param => param._id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
@@ -182,12 +184,17 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, name) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field'))
this.selectedParam = name;
selectParameter (event, uid) {
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
@@ -196,6 +203,7 @@ export default {
},
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`,
type: 'INT',
context: 'IN',
@@ -209,8 +217,8 @@ export default {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20);
},
removeParameter (name) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name);
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID();
@@ -223,7 +231,7 @@ export default {
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : '';
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
}
}
};
@@ -242,23 +250,16 @@ export default {
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-param {
background: $bg-color-light;
&.selected-element {
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 300px;
overflow: auto;

View File

@@ -2,6 +2,7 @@
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
class="options-modal"
@confirm="confirmIndexesChange"
@hide="$emit('hide')"
>
@@ -36,7 +37,7 @@
v-for="index in indexesProxy"
:key="index._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-index': selectedIndexID === index._id}"
:class="{'selected-element': selectedIndexID === index._id}"
@click="selectIndex($event, index._id)"
>
<div class="tile-icon">
@@ -256,23 +257,16 @@ export default {
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-index {
background: $bg-color-light;
&.selected-element {
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 300px;
overflow: auto;

View File

@@ -26,7 +26,7 @@
>
</div>
</div>
<div class="form-group">
<div v-if="workspace.customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
@@ -38,7 +38,7 @@
>
</div>
</div>
<div class="form-group">
<div v-if="workspace.customizations.autoIncrement" class="form-group">
<label class="form-label col-4">
{{ $t('word.autoIncrement') }}
</label>
@@ -50,7 +50,7 @@
>
</div>
</div>
<div class="form-group">
<div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4">
{{ $t('word.collation') }}
</label>
@@ -66,7 +66,7 @@
</select>
</div>
</div>
<div class="form-group">
<div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4">
{{ $t('word.engine') }}
</label>

View File

@@ -2,7 +2,8 @@
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmIndexesChange"
class="options-modal"
@confirm="confirmParametersChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
@@ -34,14 +35,14 @@
<div ref="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param.name"
:key="param._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}"
@click="selectParameter($event, param.name)"
:class="{'selected-element': selectedParam === param._id}"
@click="selectParameter($event, param._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" />
<i class="mdi mdi-hexagon mdi-24px" :class="typeClass(param.type)" />
</div>
</div>
<div class="tile-content">
@@ -54,7 +55,7 @@
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeParameter(param.name)"
@click.prevent="removeParameter(param._id)"
>
<i class="mdi mdi-close" />
</button>
@@ -170,6 +171,7 @@
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
@@ -193,7 +195,7 @@ export default {
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam);
return this.parametersProxy.find(param => param._id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
@@ -213,12 +215,17 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, name) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field'))
this.selectedParam = name;
selectParameter (event, uid) {
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
@@ -227,6 +234,7 @@ export default {
},
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
_id: uidGen(),
name: `Param${this.i++}`,
type: 'INT',
context: 'IN',
@@ -240,8 +248,8 @@ export default {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20);
},
removeParameter (name) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name);
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._id !== uid);
if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID();
@@ -254,7 +262,7 @@ export default {
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : '';
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._id : '';
}
}
};
@@ -273,23 +281,16 @@ export default {
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-param {
background: $bg-color-light;
&.selected-element {
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 300px;
overflow: auto;

View File

@@ -138,7 +138,7 @@
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import { mask } from 'vue-the-mask';
import { VueMaskDirective } from 'v-mask';
import moment from 'moment';
export default {
@@ -147,7 +147,7 @@ export default {
ConfirmModal
},
directives: {
mask
mask: VueMaskDirective
},
props: {
localOptions: Object,

View File

@@ -433,11 +433,11 @@ export default {
_id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '',
type: 'int',
type: this.workspace.dataTypes[0].types[0].name,
schema: this.schema,
table: this.table,
numPrecision: null,
numLength: 11,
numLength: this.workspace.dataTypes[0].types[0].length,
datePrecision: null,
charLength: null,
nullable: false,

View File

@@ -25,9 +25,9 @@
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm disabled"
class="btn btn-dark btn-sm"
:disabled="isChanged"
@click="false"
@click="runFunctionCheck"
>
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
@@ -70,15 +70,23 @@
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localFunction"
@confirm="runFunction"
@close="hideAskParamsModal"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsFunctionOptionsModal from '@/components/WorkspacePropsFunctionOptionsModal';
import WorkspacePropsFunctionParamsModal from '@/components/WorkspacePropsFunctionParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Functions from '@/ipc-api/Functions';
export default {
@@ -87,7 +95,8 @@ export default {
BaseLoader,
QueryEditor,
WorkspacePropsFunctionOptionsModal,
WorkspacePropsFunctionParamsModal
WorkspacePropsFunctionParamsModal,
ModalAskParameters
},
props: {
connection: Object,
@@ -100,6 +109,7 @@ export default {
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
localFunction: { sql: '' },
lastFunction: null,
@@ -165,7 +175,8 @@ export default {
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
async getFunctionData () {
if (!this.function) return;
@@ -183,6 +194,12 @@ export default {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success') {
this.originalFunction = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => {
param._id = uidGen();
return param;
})];
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.sqlProxy = this.localFunction.sql;
}
@@ -250,6 +267,31 @@ export default {
parametersUpdate (parameters) {
this.localFunction = { ...this.localFunction, parameters };
},
runFunctionCheck () {
if (this.localFunction.parameters.length)
this.showAskParamsModal();
else
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql;
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
@@ -261,6 +303,12 @@ export default {
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
}
}
};

View File

@@ -25,9 +25,9 @@
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm disabled"
class="btn btn-dark btn-sm"
:disabled="isChanged"
@click="false"
@click="runRoutineCheck"
>
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
@@ -47,7 +47,8 @@
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor
v-if="isSelected"
v-show="isSelected"
:key="`${routine}-${_uid}`"
ref="queryEditor"
:value.sync="localRoutine.sql"
:workspace="workspace"
@@ -70,15 +71,23 @@
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localRoutine"
@confirm="runRoutine"
@close="hideAskParamsModal"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Routines from '@/ipc-api/Routines';
export default {
@@ -87,7 +96,8 @@ export default {
QueryEditor,
BaseLoader,
WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal
WorkspacePropsRoutineParamsModal,
ModalAskParameters
},
props: {
connection: Object,
@@ -100,6 +110,7 @@ export default {
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalRoutine: null,
localRoutine: { sql: '' },
lastRoutine: null,
@@ -165,7 +176,8 @@ export default {
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab'
}),
async getRoutineData () {
if (!this.routine) return;
@@ -182,6 +194,12 @@ export default {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') {
this.originalRoutine = response;
this.originalRoutine.parameters = [...this.originalRoutine.parameters.map(param => {
param._id = uidGen();
return param;
})];
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.sqlProxy = this.localRoutine.sql;
}
@@ -249,6 +267,31 @@ export default {
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
runRoutineCheck () {
if (this.localRoutine.parameters.length)
this.showAskParamsModal();
else
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql;
switch (this.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.originalRoutine.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.originalRoutine.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
@@ -260,6 +303,12 @@ export default {
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
}
}
};

View File

@@ -118,7 +118,7 @@
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label>
<QueryEditor
v-if="isSelected"
v-show="isSelected"
ref="queryEditor"
:value.sync="localScheduler.sql"
:workspace="workspace"

View File

@@ -99,7 +99,7 @@
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor
v-if="isSelected"
v-show="isSelected"
ref="queryEditor"
:value.sync="localTrigger.sql"
:workspace="workspace"

View File

@@ -37,7 +37,7 @@
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
@@ -68,7 +68,7 @@
</div>
<div class="columns">
<div class="column col-auto mr-2">
<div class="form-group">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
<input
@@ -91,7 +91,7 @@
</div>
</div>
<div class="column col-auto mr-2">
<div class="form-group">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
<input
@@ -122,7 +122,7 @@
</label>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio">

View File

@@ -49,21 +49,21 @@
</div>
</div>
</div>
<div class="th">
<div v-if="customizations.unsigned" class="th">
<div class="column-resizable">
<div class="table-column-title">
{{ $t('word.unsigned') }}
</div>
</div>
</div>
<div class="th">
<div v-if="customizations.nullable" class="th">
<div class="column-resizable">
<div class="table-column-title">
{{ $t('message.allowNull') }}
</div>
</div>
</div>
<div class="th">
<div v-if="customizations.zerofill" class="th">
<div class="column-resizable">
<div class="table-column-title">
{{ $t('message.zeroFill') }}
@@ -77,14 +77,14 @@
</div>
</div>
</div>
<div class="th">
<div v-if="customizations.comment" class="th">
<div class="column-resizable">
<div class="table-column-title">
{{ $t('word.comment') }}
</div>
</div>
</div>
<div class="th">
<div v-if="customizations.collation" class="th">
<div class="column-resizable min-100">
<div class="table-column-title">
{{ $t('word.collation') }}
@@ -106,6 +106,7 @@
:indexes="getIndexes(row.name)"
:foreigns="getForeigns(row.name)"
:data-types="dataTypes"
:customizations="customizations"
@contextmenu="contextMenu"
/>
</draggable>
@@ -154,6 +155,9 @@ export default {
workspaceSchema () {
return this.getWorkspace(this.connUid).breadcrumbs.schema;
},
customizations () {
return this.getWorkspace(this.connUid).customizations;
},
dataTypes () {
return this.getWorkspace(this.connUid).dataTypes;
},

View File

@@ -1,8 +1,8 @@
<template>
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)">
<div class="td" tabindex="0">
<div class="row-draggable">
<i class="mdi mdi-drag-horizontal row-draggable-icon" />
<div :class="customizations.sortableFields ? 'row-draggable' : 'text-center'">
<i v-if="customizations.sortableFields" class="mdi mdi-drag-horizontal row-draggable-icon" />
{{ localRow.order }}
</div>
</div>
@@ -48,7 +48,7 @@
<span
v-if="!isInlineEditor.type"
class="cell-content text-left"
:class="`type-${lowerCase(localRow.type)}`"
:class="typeClass(localRow.type)"
@click="editON($event, localRow.type.toUpperCase(), 'type')"
>
{{ localRow.type }}
@@ -96,7 +96,11 @@
>
</template>
</div>
<div class="td" tabindex="0">
<div
v-if="customizations.unsigned"
class="td"
tabindex="0"
>
<label class="form-checkbox">
<input
v-model="localRow.unsigned"
@@ -106,7 +110,11 @@
<i class="form-icon" />
</label>
</div>
<div class="td" tabindex="0">
<div
v-if="customizations.nullable"
class="td"
tabindex="0"
>
<label class="form-checkbox">
<input
v-model="localRow.nullable"
@@ -116,7 +124,11 @@
<i class="form-icon" />
</label>
</div>
<div class="td" tabindex="0">
<div
v-if="customizations.zerofill"
class="td"
tabindex="0"
>
<label class="form-checkbox">
<input
v-model="localRow.zerofill"
@@ -131,7 +143,11 @@
{{ fieldDefault }}
</span>
</div>
<div class="td type-varchar" tabindex="0">
<div
v-if="customizations.comment"
class="td type-varchar"
tabindex="0"
>
<span
v-if="!isInlineEditor.comment"
class="cell-content"
@@ -149,7 +165,11 @@
@blur="editOFF"
>
</div>
<div class="td" tabindex="0">
<div
v-if="customizations.collation"
class="td"
tabindex="0"
>
<template v-if="fieldType.collation">
<span
v-if="!isInlineEditor.collation"
@@ -220,7 +240,7 @@
</div>
</div>
</div>
<div class="mb-2">
<div v-if="customizations.nullable" class="mb-2">
<label class="form-radio form-inline">
<input
v-model="defaultValue.type"
@@ -230,7 +250,7 @@
><i class="form-icon" /> NULL
</label>
</div>
<div class="mb-2">
<div v-if="customizations.autoIncrement" class="mb-2">
<label class="form-radio form-inline">
<input
v-model="defaultValue.type"
@@ -261,7 +281,7 @@
</div>
</div>
</div>
<div>
<div v-if="customizations.onUpdate">
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.onUpdate') }}
@@ -294,7 +314,8 @@ export default {
row: Object,
dataTypes: Array,
indexes: Array,
foreigns: Array
foreigns: Array,
customizations: Object
},
data () {
return {
@@ -378,10 +399,10 @@ export default {
return 'UNKNOWN ' + key;
}
},
lowerCase (val) {
if (val)
return val.toLowerCase();
return val;
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
initLocalRow () {
Object.keys(this.localRow).forEach(key => {

View File

@@ -2,12 +2,13 @@
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<QueryEditor
v-if="isSelected"
v-show="isSelected"
ref="queryEditor"
:auto-focus="true"
:value.sync="query"
:workspace="workspace"
:schema="schema"
:is-selected="isSelected"
:height="editorHeight"
/>
<div ref="resizer" class="query-area-resizer" />
@@ -17,7 +18,7 @@
class="btn btn-primary btn-sm"
:class="{'loading':isQuering}"
:disabled="!query"
title="F9"
title="F5"
@click="runQuery(query)"
>
<span>{{ $t('word.run') }}</span>
@@ -25,6 +26,13 @@
</button>
</div>
<div class="workspace-query-info">
<div
v-if="results.length"
class="d-flex"
:title="$t('message.queryDuration')"
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ durationsCount / 1000 }}s</b>
</div>
<div v-if="resultsCount">
{{ $t('word.results') }}: <b>{{ resultsCount.toLocaleString() }}</b>
</div>
@@ -41,14 +49,16 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12">
<div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" />
<WorkspaceQueryTable
v-if="results"
v-show="!isQuering"
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:tab-uid="tab.uid"
:conn-uid="connection.uid"
:is-selected="isSelected"
mode="query"
@update-field="updateField"
@delete-selected="deleteSelected"
@@ -58,8 +68,9 @@
</template>
<script>
import Database from '@/ipc-api/Database';
import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs';
@@ -67,13 +78,14 @@ import tableTabs from '@/mixins/tableTabs';
export default {
name: 'WorkspaceQueryTab',
components: {
BaseLoader,
QueryEditor,
WorkspaceQueryTable
},
mixins: [tableTabs],
props: {
connection: Object,
tabUid: String,
tab: Object,
isSelected: Boolean
},
data () {
@@ -83,19 +95,26 @@ export default {
isQuering: false,
results: [],
resultsCount: 0,
durationsCount: 0,
affectedCount: 0,
editorHeight: 200
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isWorkspaceSelected () {
return this.workspace.uid === this.selectedWorkspace;
}
},
created () {
this.query = this.tab.content;
window.addEventListener('keydown', this.onKey);
},
mounted () {
@@ -107,6 +126,9 @@ export default {
window.addEventListener('mousemove', this.resize);
window.addEventListener('mouseup', this.stopResize);
});
if (this.tab.autorun)
this.runQuery(this.query);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
@@ -128,11 +150,12 @@ export default {
query
};
const { status, response } = await Database.rawQuery(params);
const { status, response } = await Schema.rawQuery(params);
if (status === 'success') {
this.results = Array.isArray(response) ? response : [response];
this.resultsCount += this.results.reduce((acc, curr) => acc + (curr.rows ? curr.rows.length : 0), 0);
this.durationsCount += this.results.reduce((acc, curr) => acc + curr.duration, 0);
this.affectedCount += this.results.reduce((acc, curr) => acc + (curr.report ? curr.report.affectedRows : 0), 0);
}
else
@@ -151,6 +174,7 @@ export default {
clearTabData () {
this.results = [];
this.resultsCount = 0;
this.durationsCount = 0;
this.affectedCount = 0;
},
resize (e) {
@@ -169,9 +193,9 @@ export default {
this.$refs.queryEditor.editor.resize();
},
onKey (e) {
if (this.isSelected) {
if (this.isSelected && this.isWorkspaceSelected) {
e.stopPropagation();
if (e.key === 'F9')
if (e.key === 'F5')
this.runQuery(this.query);
}
}
@@ -208,7 +232,6 @@ export default {
.btn {
display: flex;
align-self: center;
color: $body-font-color;
margin-right: 0.4rem;
}
}
@@ -222,6 +245,10 @@ export default {
}
}
}
.workspace-query-results {
min-height: 200px;
}
}
</style>

View File

@@ -9,6 +9,7 @@
:context-event="contextEvent"
:selected-rows="selectedRows"
@delete-selected="deleteSelected"
@set-null="setNull"
@close-context="isContext = false"
/>
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
@@ -41,7 +42,7 @@
/>
<span>{{ field.alias || field.name }}</span>
<i
v-if="currentSort === field.name || currentSort === `${field.table}.${field.name}`"
v-if="isSortable && currentSort === field.name || currentSort === `${field.table}.${field.name}`"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
@@ -64,9 +65,8 @@
v-for="row in items"
:key="row._id"
:row="row"
:fields="fields"
:fields="fieldsObj"
:key-usage="keyUsage"
class="tr"
:class="{'selected': selectedRows.includes(row._id)}"
@select-row="selectRow($event, row._id)"
@update-field="updateField($event, row)"
@@ -96,9 +96,9 @@ export default {
},
props: {
results: Array,
tabUid: [String, Number],
connUid: String,
mode: String
mode: String,
isSelected: Boolean
},
data () {
return {
@@ -122,7 +122,15 @@ export default {
return this.getWorkspace(this.connUid).breadcrumbs.schema;
},
primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
const primaryFields = this.fields.filter(field => ['pri', 'uni'].includes(field.key));
if (primaryFields.length > 1 || !primaryFields.length)
return false;
return primaryFields[0];
},
isSortable () {
return this.fields.every(field => field.name);
},
isHardSort () {
return this.mode === 'table' && this.localResults.length === 1000;
@@ -150,6 +158,37 @@ export default {
},
keyUsage () {
return this.resultsWithRows.length ? this.resultsWithRows[this.resultsetIndex].keys : [];
},
fieldsObj () {
if (this.sortedResults.length) {
const fieldsObj = {};
for (const key in this.sortedResults[0]) {
if (key === '_id') continue;
const fieldObj = this.fields.find(field => {
let fieldNames = [
field.name,
field.alias,
`${field.table}.${field.name}`,
`${field.table}.${field.alias}`,
`${field.tableAlias}.${field.name}`,
`${field.tableAlias}.${field.alias}`
];
if (field.table)
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
if (field.tableAlias)
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
return fieldNames.includes(key);
});
fieldsObj[key] = fieldObj;
}
return fieldsObj;
}
return {};
}
},
watch: {
@@ -240,7 +279,7 @@ export default {
: [];
},
resizeResults () {
if (this.$refs.resultTable) {
if (this.$refs.resultTable && this.isSelected) {
const el = this.$refs.tableWrapper;
if (el) {
@@ -255,14 +294,17 @@ export default {
this.resizeResults();
},
updateField (payload, row) {
const orgRow = this.localResults.find(lr => lr._id === row._id);
delete row._id;
delete orgRow._id;
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(row),
id: this.getPrimaryValue(orgRow),
row,
orgRow,
...payload
};
this.$emit('update-field', params);
@@ -281,6 +323,22 @@ export default {
};
this.$emit('delete-selected', params);
},
setNull () {
const row = this.localResults.find(row => this.selectedRows.includes(row._id));
delete row._id;
const params = {
primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(row),
row,
orgRow: row,
field: this.selectedCell.field,
content: null
};
this.$emit('update-field', params);
},
applyUpdate (params) {
const { primary, id, field, table, content } = params;
@@ -321,6 +379,8 @@ export default {
this.selectedRows = [row];
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.selectedCell = cell;
if (!this.selectedRows.includes(cell.id))
this.selectedRows = [cell.id];
@@ -328,6 +388,8 @@ export default {
this.isContext = true;
},
sort (field) {
if (!this.isSortable) return;
if (this.mode === 'query')
field = `${this.getTable(this.resultsetIndex)}.${field}`;

View File

@@ -3,8 +3,19 @@
:context-event="contextEvent"
@close-context="closeContext"
>
<div
v-if="selectedRows.length === 1"
class="context-element"
@click="setNull"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-null text-light pr-1" /> {{ $t('message.setNull') }}
</span>
</div>
<div class="context-element" @click="showConfirmModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}</span>
<span class="d-flex">
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</span>
</div>
<ConfirmModal
@@ -60,6 +71,10 @@ export default {
deleteRows () {
this.$emit('delete-selected');
this.closeContext();
},
setNull () {
this.$emit('set-null');
this.closeContext();
}
}
};

View File

@@ -6,15 +6,15 @@
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})"
@contextmenu.prevent="openContext($event, { id: row._id, field: cKey })"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${getFieldType(cKey)}`"
:class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(getFieldType(cKey), getFieldPrecision(cKey)) | cutText }}</span>
>{{ col | typeFormat(fields[cKey].type.toLowerCase(), fields[cKey].length) | cutText }}</span>
<ForeignKeySelect
v-else-if="isForeignKey(cKey)"
class="editable-field"
@@ -34,6 +34,15 @@
class="editable-field px-2"
@blur="editOFF"
>
<select
v-else-if="inputProps.type === 'boolean'"
v-model="editingContent"
class="form-select small-select editable-field"
@blur="editOFF"
>
<option>true</option>
<option>false</option>
</select>
<input
v-else
ref="editField"
@@ -61,14 +70,52 @@
<div :slot="'body'">
<div class="mb-2">
<div>
<textarea
v-model="editingContent"
class="form-input textarea-editor"
<TextEditor
:value.sync="editingContent"
editor-class="textarea-editor"
:mode="editorMode"
/>
</div>
<div class="editor-field-info">
<div><b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}</div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
<div class="editor-field-info p-vcentered">
<div class="d-flex p-vcentered">
<label for="editorMode" class="form-label mr-2">
<b>{{ $t('word.content') }}</b>:
</label>
<select
id="editorMode"
v-model="editorMode"
class="form-select select-sm"
>
<option value="text">
TEXT
</option>
<option value="html">
HTML
</option>
<option value="xml">
XML
</option>
<option value="json">
JSON
</option>
<option value="svg">
SVG
</option>
<option value="yaml">
YAML
</option>
</select>
</div>
<div class="d-flex">
<div class="p-vcentered">
<div class="mr-4">
<b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}
</div>
<div>
<b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -135,19 +182,21 @@ import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
name: 'WorkspaceQueryTableRow',
components: {
ConfirmModal,
TextEditor,
ForeignKeySelect
},
directives: {
mask
mask: VueMaskDirective
},
filters: {
formatBytes,
@@ -164,6 +213,9 @@ export default {
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
if (DATETIME.includes(type)) {
if (typeof val === 'string')
return val;
let datePrecision = '';
for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S';
@@ -185,15 +237,18 @@ export default {
return hexToBinary(hex);
}
if (ARRAY.includes(type)) {
if (Array.isArray(val))
return JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
return val;
}
return val;
}
},
props: {
row: Object,
fields: {
type: Array,
default: () => []
},
fields: Object,
keyUsage: Array
},
data () {
@@ -206,6 +261,8 @@ export default {
editingContent: null,
editingType: null,
editingField: null,
editingLength: null,
editorMode: 'text',
contentInfo: {
ext: '',
mime: '',
@@ -224,7 +281,7 @@ export default {
if (TIME.includes(this.editingType)) {
let timeMask = '##:##:##';
const precision = this.getFieldPrecision(this.editingField);
const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
@@ -237,7 +294,7 @@ export default {
if (DATETIME.includes(this.editingType)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.getFieldPrecision(this.editingField);
const precision = this.fields[this.editingField].length;
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
@@ -248,8 +305,8 @@ export default {
if (BLOB.includes(this.editingType))
return { type: 'file', mask: false };
if (BIT.includes(this.editingType))
return { type: 'text', mask: false };
if (BOOLEAN.includes(this.editingType))
return { type: 'boolean', mask: false };
return { type: 'text', mask: false };
},
@@ -261,15 +318,15 @@ export default {
},
isEditable () {
if (this.fields) {
const nElements = this.fields.reduce((acc, curr) => {
acc.add(curr.table);
acc.add(curr.schema);
const nElements = Object.keys(this.fields).reduce((acc, curr) => {
acc.add(this.fields[curr].table);
acc.add(this.fields[curr].schema);
return acc;
}, new Set([]));
if (nElements.size > 2) return false;
return !!(this.fields[0].schema && this.fields[0].table);
return !!(this.fields[Object.keys(this.fields)[0]].schema && this.fields[Object.keys(this.fields)[0]].table);
}
return false;
@@ -277,7 +334,7 @@ export default {
},
watch: {
fields () {
this.fields.forEach(field => {
Object.keys(this.fields).forEach(field => {
this.isInlineEditor[field.name] = false;
});
}
@@ -289,69 +346,41 @@ export default {
return this.foreignKeys.includes(key);
},
getFieldType (cKey) {
let type = 'unknown';
const field = this.getFieldObj(cKey);
if (field)
type = field.type;
return type.toLowerCase();
},
getFieldPrecision (cKey) {
let length = 0;
const field = this.getFieldObj(cKey);
if (field)
length = field.datePrecision;
return length;
},
getFieldObj (cKey) {
return this.fields.filter(field => {
let fieldNames = [
field.name,
field.alias,
`${field.table}.${field.name}`,
`${field.table}.${field.alias}`,
`${field.tableAlias}.${field.name}`,
`${field.tableAlias}.${field.alias}`
];
if (field.table)
fieldNames = [...fieldNames, `${field.table.toLowerCase()}.${field.name}`, `${field.table.toLowerCase()}.${field.alias}`];
if (field.tableAlias)
fieldNames = [...fieldNames, `${field.tableAlias.toLowerCase()}.${field.name}`, `${field.tableAlias.toLowerCase()}.${field.alias}`];
return fieldNames.includes(cKey);
})[0];
},
isNull (value) {
return value === null ? ' is-null' : '';
},
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
bufferToBase64 (val) {
return bufferToBase64(val);
},
editON (event, content, field) {
if (!this.isEditable) return;
const type = this.getFieldType(field).toUpperCase(); ;
this.originalContent = content;
window.addEventListener('keydown', this.onKey);
const type = this.fields[field].type.toUpperCase(); ;
this.originalContent = this.$options.filters.typeFormat(content, type, this.fields[field].length);
this.editingType = type;
this.editingField = field;
this.editingLength = this.fields[field].length;
if (LONG_TEXT.includes(type)) {
if ([...LONG_TEXT, ...ARRAY, ...TEXT_SEARCH].includes(type)) {
this.isTextareaEditor = true;
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type);
this.editingContent = this.$options.filters.typeFormat(content, type);
return;
}
if (BLOB.includes(type)) {
this.isBlobEditor = true;
this.editingContent = this.originalContent || '';
this.editingContent = content || '';
this.fileToUpload = null;
this.willBeDeleted = false;
if (this.originalContent !== null) {
if (content !== null) {
const buff = Buffer.from(this.editingContent);
if (buff.length) {
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
@@ -367,7 +396,7 @@ export default {
}
// Inline editable fields
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.getFieldPrecision(field));
this.editingContent = this.originalContent;
this.$nextTick(() => { // Focus on input
event.target.blur();
@@ -378,10 +407,18 @@ export default {
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
},
editOFF () {
if (!this.editingField) return;
this.isInlineEditor[this.editingField] = false;
let content;
if (!BLOB.includes(this.editingType)) {
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType)) return;// If not changed
if ([...DATETIME, ...TIME].includes(this.editingType)) {
if (this.editingContent.substring(this.editingContent.length - 1) === '.')
this.editingContent = this.editingContent.slice(0, -1);
}
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType, this.editingLength)) return;// If not changed
content = this.editingContent;
}
else { // Handle file upload
@@ -396,13 +433,14 @@ export default {
}
this.$emit('update-field', {
field: this.getFieldObj(this.editingField).name,
field: this.fields[this.editingField].name,
type: this.editingType,
content
});
this.editingType = null;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
},
hideEditorModal () {
this.isTextareaEditor = false;
@@ -434,9 +472,6 @@ export default {
};
this.willBeDeleted = true;
},
contextMenu (event, cell) {
this.$emit('update-field', event, cell);
},
selectRow (event, row) {
this.$emit('select-row', event, row);
},
@@ -444,6 +479,20 @@ export default {
if (keyName.includes('.'))
return this.keyUsage.find(key => key.field === keyName.split('.').pop());
return this.keyUsage.find(key => key.field === keyName);
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
}
}
};
@@ -473,7 +522,7 @@ export default {
}
.editor-field-info {
margin-top: 0.6rem;
margin-top: 0.4rem;
display: flex;
justify-content: space-between;
white-space: normal;

View File

@@ -36,6 +36,7 @@
<button
v-if="isTable"
class="btn btn-dark btn-sm"
:disabled="isQuering"
@click="showFakerModal"
>
<span>{{ $t('message.tableFiller') }}</span>
@@ -63,6 +64,13 @@
</div>
</div>
<div class="workspace-query-info">
<div
v-if="results.length"
class="d-flex"
:title="$t('message.queryDuration')"
>
<i class="mdi mdi-timer-sand mdi-rotate-180 pr-1" /> <b>{{ results[0].duration / 1000 }}s</b>
</div>
<div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
</div>
@@ -75,13 +83,15 @@
</div>
</div>
</div>
<div class="workspace-query-results column col-12">
<div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" />
<WorkspaceQueryTable
v-if="results"
ref="queryTable"
:results="results"
:tab-uid="tabUid"
:conn-uid="connection.uid"
:is-selected="isSelected"
mode="table"
@update-field="updateField"
@delete-selected="deleteSelected"
@@ -109,6 +119,7 @@
<script>
import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import ModalNewTableRow from '@/components/ModalNewTableRow';
import ModalFakerRows from '@/components/ModalFakerRows';
@@ -118,6 +129,7 @@ import tableTabs from '@/mixins/tableTabs';
export default {
name: 'WorkspaceTableTab',
components: {
BaseLoader,
WorkspaceQueryTable,
ModalNewTableRow,
ModalFakerRows
@@ -142,13 +154,14 @@ export default {
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'data';
return this.workspace.selected_tab === 'data' && this.workspace.uid === this.selectedWorkspace;
},
isTable () {
return !!this.workspace.breadcrumbs.table;
@@ -242,6 +255,7 @@ export default {
this.isAddModal = false;
},
showFakerModal () {
if (this.isQuering) return;
this.isFakerModal = true;
},
hideFakerModal () {
@@ -275,10 +289,6 @@ export default {
.export-dropdown {
.menu {
min-width: 100%;
.menu-item a:hover {
background: $bg-color-gray;
}
}
}
</style>

View File

@@ -93,7 +93,16 @@ module.exports = {
ciphers: 'Ciphers',
upload: 'Upload',
browse: 'Browse',
faker: 'Faker'
faker: 'Faker',
content: 'Content',
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
tools: 'Tools',
variables: 'Variables',
processes: 'Processes',
database: 'Database',
scratchpad: 'Scratchpad'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -184,7 +193,20 @@ module.exports = {
enableSsl: 'Enable SSL',
manualValue: 'Manual value',
tableFiller: 'Table Filler',
fakeDataLanguage: 'Fake data language'
fakeDataLanguage: 'Fake data language',
searchForElements: 'Search for elements',
selectAll: 'Select all',
queryDuration: 'Query duration',
includeBetaUpdates: 'Include beta updates',
setNull: 'Set NULL',
processesList: 'Processes list',
processInfo: 'Process info',
manageUsers: 'Manage users',
createNewSchema: 'Create new schema',
schemaName: 'Schema name',
editSchema: 'Edit schema',
deleteSchema: 'Delete schema',
markdownSupported: 'Markdown supported'
},
faker: {
address: 'Address',

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -2,20 +2,20 @@
import { ipcRenderer } from 'electron';
export default class {
static createDatabase (params) {
return ipcRenderer.invoke('create-database', params);
static createSchema (params) {
return ipcRenderer.invoke('create-schema', params);
}
static updateDatabase (params) {
return ipcRenderer.invoke('update-database', params);
static updateSchema (params) {
return ipcRenderer.invoke('update-schema', params);
}
static getDatabaseCollation (params) {
return ipcRenderer.invoke('get-database-collation', params);
return ipcRenderer.invoke('get-schema-collation', params);
}
static deleteDatabase (params) {
return ipcRenderer.invoke('delete-database', params);
static deleteSchema (params) {
return ipcRenderer.invoke('delete-schema', params);
}
static getStructure (params) {
@@ -38,6 +38,10 @@ export default class {
return ipcRenderer.invoke('get-version', uid);
}
static getProcesses (uid) {
return ipcRenderer.invoke('get-processes', uid);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}

View File

@@ -22,6 +22,14 @@
"mediumtext": $string-color,
"longtext": $string-color,
"json": $string-color,
"name": $string-color,
"character": $string-color,
"character_varying": $string-color,
"cidr": $string-color,
"inet": $string-color,
"macaddr": $string-color,
"macaddr8": $string-color,
"uuid": $string-color,
"int": $number-color,
"tinyint": $number-color,
"smallint": $number-color,
@@ -30,20 +38,44 @@
"double": $number-color,
"decimal": $number-color,
"bigint": $number-color,
"newdecimal": $number-color,
"integer": $number-color,
"numeric": $number-color,
"smallserial": $number-color,
"serial": $number-color,
"bigserial": $number-color,
"real": $number-color,
"double_precision": $number-color,
"oid": $number-color,
"xid": $number-color,
"money": $number-color,
"datetime": $date-color,
"date": $date-color,
"time": $date-color,
"time_with_time_zone": $date-color,
"year": $date-color,
"timestamp": $date-color,
"timestamp_without_time_zone": $date-color,
"timestamp_with_time_zone": $date-color,
"bit": $bit-color,
"bit_varying": $bit-color,
"binary": $blob-color,
"varbinary": $blob-color,
"blob": $blob-color,
"tinyblob": $blob-color,
"mediumblob": $blob-color,
"medium_blob": $blob-color,
"longblob": $blob-color,
"bytea": $blob-color,
"enum": $enum-color,
"set": $enum-color,
"boolean": $enum-color,
"interval": $array-color,
"array": $array-color,
"anyarray": $array-color,
"tsvector": $array-color,
"tsquery": $array-color,
"pg_node_tree": $array-color,
"unknown": $unknown-color,
)
);

View File

@@ -27,7 +27,6 @@
.td,
.th {
border-bottom: $border-width solid $border-color;
padding: $unit-3 $unit-2;
display: table-cell;
}
@@ -35,37 +34,4 @@
.th {
border-bottom-width: $border-width-lg;
}
&,
&.table-striped {
.tbody {
.tr {
&.selected {
background: #333 !important;
}
&.active {
background: $bg-color-dark;
}
}
}
}
&.table-hover {
.tbody {
.tr {
&:hover {
background: $bg-color-dark;
}
}
}
}
&.table-striped {
.tbody {
.tr:nth-of-type(odd) {
background: $bg-color;
}
}
}
}

View File

@@ -15,7 +15,8 @@
}
&.key-mul,
&.key-INDEX {
&.key-INDEX,
&.key-KEY {
color: palegreen;
}

View File

@@ -1,9 +1,12 @@
/* Colors */
$body-bg: #1d1d1d;
$body-font-color: #fff;
$bg-color: #1d1d1d;
$bg-color-light: #3f3f3f;
$body-bg: #fdfdfd;
$body-bg-dark: #1d1d1d;
$body-font-color-dark: #fff;
$bg-color-dark: #1d1d1d;
$bg-color-light-dark: #3f3f3f;
$bg-color-gray: #272727;
$bg-color-light-gray: #f1f1f1;
$light-color: #fdfdfd;
$primary-color: #e36929;
$success-color: #32b643;
$error-color: #de3b28;
@@ -14,7 +17,8 @@ $number-color: cornflowerblue;
$date-color: coral;
$bit-color: lightskyblue;
$blob-color: darkorchid;
$enum-color: gold;
$array-color: yellowgreen;
$enum-color: goldenrod;
$unknown-color: gray;
/* Sizes */

View File

@@ -6,6 +6,8 @@
@import "fake-tables";
@import "mdi-additions";
@import "db-icons";
@import "themes/dark-theme";
@import "themes/light-theme";
@import "~spectre.css/src/spectre";
@import "~spectre.css/src/spectre-exp";
@@ -19,27 +21,6 @@ body {
@include padding-variant(3, $unit-3);
@include padding-variant(4, $unit-4);
.btn {
&.btn-gray {
color: #fff;
background: $bg-color-gray;
&:hover {
background: $bg-color;
}
}
&.btn-dark {
color: #fff;
background: $bg-color-light;
border-color: $bg-color-light;
&:hover {
background: $bg-color-gray;
}
}
}
.p-vcentered {
display: flex !important;
align-items: center;
@@ -55,15 +36,6 @@ body {
box-shadow: none !important;
}
.bg-checkered {
background-image:
linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
linear-gradient(to right, black 50%, white 50%),
linear-gradient(to bottom, black 50%, white 50%);
background-blend-mode: normal, difference, normal;
background-size: 2em 2em;
}
.workspace-tabs {
align-content: baseline;
@@ -80,13 +52,14 @@ body {
.btn {
display: flex;
align-self: center;
color: $body-font-color;
margin-right: 0.4rem;
}
}
.workspace-query-info {
display: flex;
overflow: hidden;
white-space: nowrap;
> div + div {
padding-left: 0.6rem;
@@ -96,24 +69,16 @@ body {
}
}
.process-row .td:last-child {
width: 100%;
}
// Scrollbars
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: $bg-color-light;
}
::-webkit-scrollbar-thumb {
background: rgba($color: #fff, $alpha: 0.5);
&:hover {
background: rgba($color: #fff, $alpha: 1);
}
}
// Animations
@keyframes rotation {
from {
@@ -131,32 +96,21 @@ body {
/* Override */
.modal {
.modal-overlay,
&.active .modal-overlay {
background: rgba(255, 255, 255, 0.15);
}
.modal-container,
.modal-sm .modal-container {
box-shadow: 0 0 1px 0 #000;
padding: 0;
background: $bg-color;
.modal-header {
padding: 0.4rem 0.8rem;
text-transform: uppercase;
background: $bg-color-gray;
display: flex;
justify-content: space-between;
align-items: center;
color: #fff;
}
}
}
.tab {
border-color: #272727;
.tab-item {
.btn-clear {
margin-top: -0.1rem;
@@ -188,34 +142,11 @@ body {
}
}
.form-select,
.form-input,
.form-select:not([multiple]):not([size]),
.form-checkbox .form-icon,
.form-radio .form-icon {
border-color: $bg-color-light;
background-color: $bg-color-gray;
}
.form-input.is-error,
.form-select.is-error {
background-color: $bg-color-gray;
}
.form-input:not(:placeholder-shown):invalid:focus {
background: $bg-color-gray;
}
.form-select:not([multiple]):not([size]):focus {
border-color: $primary-color;
}
.form-input[type="file"] {
overflow: hidden;
}
.input-group .input-group-addon {
border-color: #3f3f3f;
z-index: 1;
}
@@ -239,7 +170,3 @@ body {
visibility: hidden;
}
}
.empty {
color: $body-font-color;
}

View File

@@ -0,0 +1,435 @@
.theme-dark {
color: $body-font-color-dark;
background: $body-bg-dark;
::-webkit-scrollbar-track {
background: $bg-color-light-dark;
}
::-webkit-scrollbar-thumb {
background: rgba($color: #fff, $alpha: 0.5);
&:hover {
background: rgba($color: #fff, $alpha: 1);
}
}
:disabled {
.file-uploader {
background-color: #151515;
}
}
// Override Spectre.css
.menu {
background: $bg-color-light-dark;
.menu-item a {
&:hover {
color: $primary-color;
background: $bg-color-gray;
}
}
}
.btn {
&.btn-link {
color: rgba($body-font-color-dark, 0.8);
&:hover {
color: $body-font-color-dark;
}
}
&.btn-gray {
color: #fff;
background: $bg-color-gray;
&:hover {
background: $bg-color-dark;
}
}
&.btn-dark {
color: #fff;
background: $bg-color-light-dark;
border-color: $bg-color-light-dark;
&:hover {
background: $bg-color-gray;
}
}
}
.modal {
.modal-overlay,
&.active .modal-overlay {
background: rgba(255, 255, 255, 0.15);
}
.modal-container,
.modal-sm .modal-container {
box-shadow: 0 0 1px 0 #000;
background: $bg-color-dark;
.modal-header {
background: $bg-color-gray;
color: #fff;
}
}
}
.tab {
border-color: #272727;
}
.form-select,
.form-input,
.form-select:not([multiple]):not([size]),
.form-checkbox .form-icon,
.form-radio .form-icon {
border-color: $bg-color-light-dark;
background-color: $bg-color-gray;
color: $body-font-color-dark;
}
.form-input.is-error,
.form-select.is-error {
background-color: $bg-color-gray;
}
.form-input:not(:placeholder-shown):invalid:focus {
background: $bg-color-gray;
}
.form-select:not([multiple]):not([size]):focus {
border-color: $primary-color;
}
.input-group .input-group-addon {
border-color: #3f3f3f;
background: $bg-color-dark;
}
.empty {
color: $body-font-color-dark;
background: transparent;
}
.form-switch .form-icon::before {
background: $bg-color-light-dark;
}
// Antares
.workspace {
.workspace-explorebar {
background: $bg-color-gray;
box-shadow: 0 0 1px 0 #000;
.workspace-explorebar-database {
.database-name {
background: $bg-color-gray;
}
.database-name,
.misc-name {
&:hover {
color: $body-font-color-dark;
background: $bg-color-light-dark;
}
}
a.table-name {
&:hover {
color: inherit;
background: inherit;
}
}
.menu-item {
&:hover {
color: $body-font-color-dark;
background: rgba($color: #fff, $alpha: 0.05);
}
}
}
}
.workspace-query-results {
.table {
.th {
background: $bg-color-dark;
border-color: $bg-color-light-dark;
}
.td {
border-color: $bg-color-light-dark;
&:focus {
box-shadow: inset 0 0 0 1px $body-font-color-dark;
background: rgba($color: #000, $alpha: 0.3);
}
}
}
}
.workspace-tabs {
.tab-block {
background: $bg-color-light-dark;
.tab-item {
> a {
color: $body-font-color-dark;
}
& &.tools-dropdown {
.tab-link:focus {
color: $primary-color;
}
.menu {
.menu-item a {
&:hover {
color: $primary-color;
background: $bg-color-gray;
}
}
}
}
}
.workspace-query-runner .workspace-query-runner-footer .workspace-query-buttons .btn {
color: $body-font-color-dark;
}
}
}
}
.bg-checkered {
background-image:
linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
linear-gradient(to right, black 50%, white 50%),
linear-gradient(to bottom, black 50%, white 50%);
background-blend-mode: normal, difference, normal;
background-size: 2em 2em;
}
.context {
color: $body-font-color-dark;
.context-container {
box-shadow: 0 0 2px 0 #000;
background: #1d1d1d;
.context-element {
.context-submenu {
background: #1d1d1d;
box-shadow: 0 0 2px 0 #000;
}
&:hover {
background: $primary-color;
}
}
}
}
.editor-wrapper {
border-bottom: 1px solid #444;
}
.file-uploader {
border: 0.05rem solid $bg-color-light-dark;
background-color: $bg-color-gray;
.file-uploader-message {
border-right: 0.05rem solid $bg-color-light-dark;
background-color: $bg-color-dark;
}
}
.tile {
&:hover {
background: $bg-color-light-dark;
}
&.selected-element {
background: $bg-color-light-dark;
}
}
.editor-col {
border-left: 2px solid $bg-color-light-dark;
}
.table {
.td,
.th {
border-bottom: $border-width solid $border-color;
}
&,
&.table-striped {
.tbody {
.tr {
&.selected {
background: #333 !important;
}
&.active {
background: $bg-color-dark;
}
}
}
}
&.table-hover {
.tbody {
.tr {
&:hover {
background: #151515;
}
}
}
}
&.table-striped {
.tbody {
.tr:nth-of-type(odd) {
background: $bg-color;
}
}
}
}
#titlebar {
background: $bg-color-light-dark;
box-shadow: 0 0 1px 0 #000;
.titlebar-elements {
.titlebar-element {
&:hover {
opacity: 1;
background: rgba($color: #fff, $alpha: 0.2);
}
&.close-button:hover {
background: red;
}
}
}
}
#settingbar {
width: $settingbar-width;
height: calc(100vh - #{$excluding-size});
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
background: $bg-color-light-dark;
padding: 0;
box-shadow: 0 0 1px 0 #000;
z-index: 9;
.settingbar-top-elements {
overflow-x: hidden;
overflow-y: overlay;
max-height: calc((100vh - 3.5rem) - #{$excluding-size});
&::-webkit-scrollbar {
width: 3px;
}
}
.settingbar-bottom-elements {
padding-top: 0.5rem;
background: $bg-color-light-dark;
z-index: 1;
}
.settingbar-elements {
list-style: none;
text-align: center;
width: $settingbar-width;
padding: 0;
margin: 0;
.settingbar-element {
height: $settingbar-width;
width: 100%;
margin: 0;
border-left: 3px solid transparent;
opacity: 0.5;
transition: opacity 0.2s;
display: flex;
align-content: center;
justify-content: center;
flex-direction: column;
&:hover {
opacity: 1;
}
&.selected {
border-left-color: $body-font-color-dark;
opacity: 1;
}
.settingbar-element-icon {
&.badge::after {
bottom: -10px;
right: 0;
position: absolute;
background: $success-color;
}
&.badge-update::after {
bottom: initial;
background: $primary-color;
}
}
}
}
}
.ex-tooltip {
.ex-tooltip-content {
background: rgba(48, 55, 66, 0.95);
color: #fff;
}
&:hover .ex-tooltip-content {
visibility: visible;
opacity: 1;
}
}
#footer {
background: $primary-color;
box-shadow: 0 0 1px 0 #000;
.footer-elements {
.footer-element {
&.footer-link {
&:hover {
background: rgba($color: #fff, $alpha: 0.1);
}
}
}
}
}
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {
background-color: #c9561a99;
}
.ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover {
background-color: #c9571a33;
border: none;
}
.ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight {
color: #e0d00c;
}

View File

@@ -0,0 +1,299 @@
.theme-light {
::-webkit-scrollbar-track {
background: #fff;
}
::-webkit-scrollbar-thumb {
background: rgba($color: $bg-color-light-dark, $alpha: 0.5);
&:hover {
background: rgba($color: $bg-color-light-dark, $alpha: 1);
}
}
.form-input:disabled,
.form-input.disabled,
.form-select:disabled,
.form-select.disabled {
background: #ababab;
}
#titlebar {
background: $bg-color-light;
box-shadow: 0 0 1px 0 #000;
.titlebar-elements {
.titlebar-element {
&:hover {
opacity: 1;
background: rgba($color: rgb(172, 172, 172), $alpha: 0.2);
}
&.close-button:hover {
background: red;
}
}
}
}
.btn {
&.btn-link {
color: rgba($body-font-color, 0.8);
&:hover {
color: $body-font-color;
}
}
&.btn-gray {
color: #fff;
background: $bg-color-gray;
&:hover {
background: $bg-color-dark;
}
}
&.btn-dark {
color: #fff;
background: lighten($bg-color-light-dark, 20%);
border-color: lighten($bg-color-light-dark, 20%);
&:hover {
background: $bg-color-gray;
}
}
}
.modal {
color: $body-font-color;
&:target .modal-overlay,
&.active .modal-overlay {
background: rgba($bg-color-dark, 0.75);
}
.modal-container .modal-header {
background: $bg-color-light-dark;
color: #fff;
}
}
.empty {
background: transparent;
}
.tile {
&:hover {
background: $bg-color-light-gray;
}
&.selected-element {
background: $bg-color-light-gray;
}
}
.editor-col {
border-left: 2px solid darken($bg-color-light-gray, 15%);
}
.file-uploader {
border: 0.05rem solid $border-color-dark;
background-color: $bg-color-light;
.file-uploader-message {
border-right: 0.05rem solid $border-color-dark;
background-color: $bg-color-light;
}
}
#settingbar {
width: $settingbar-width;
height: calc(100vh - #{$excluding-size});
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
background: $bg-color-light-dark;
padding: 0;
box-shadow: 0 0 1px 0 #000;
z-index: 9;
.settingbar-top-elements {
overflow-x: hidden;
overflow-y: overlay;
max-height: calc((100vh - 3.5rem) - #{$excluding-size});
&::-webkit-scrollbar {
width: 3px;
}
}
.settingbar-bottom-elements {
padding-top: 0.5rem;
background: $bg-color-light-dark;
z-index: 1;
}
.settingbar-elements {
list-style: none;
text-align: center;
width: $settingbar-width;
padding: 0;
margin: 0;
.settingbar-element {
height: $settingbar-width;
width: 100%;
margin: 0;
border-left: 3px solid transparent;
opacity: 0.5;
transition: opacity 0.2s;
display: flex;
align-content: center;
justify-content: center;
flex-direction: column;
&:hover {
opacity: 1;
}
&.selected {
border-left-color: $body-font-color-dark;
opacity: 1;
}
.settingbar-element-icon {
&.badge::after {
bottom: -10px;
right: 0;
position: absolute;
background: $success-color;
}
&.badge-update::after {
bottom: initial;
background: $primary-color;
}
}
}
}
}
.ex-tooltip {
.ex-tooltip-content {
background: rgba(48, 55, 66, 0.95);
color: #fff;
}
&:hover .ex-tooltip-content {
visibility: visible;
opacity: 1;
}
}
.workspace {
.workspace-explorebar {
background: $bg-color-light-gray;
box-shadow: 0 0 1px 0 #000;
.workspace-explorebar-database {
.database-name {
background: $bg-color-light-gray;
}
.table-size {
opacity: 0.4;
}
}
}
.workspace-query-results {
.table {
.th {
background: $body-bg;
border-color: rgba($bg-color-light-dark, 0.5);
}
.td {
border-color: rgba($bg-color-light-dark, 0.5);
}
}
}
}
.context {
color: $body-font-color-dark;
.context-container {
box-shadow: 0 0 2px 0 #000;
background: #1d1d1d;
.context-element {
.context-submenu {
background: #1d1d1d;
box-shadow: 0 0 2px 0 #000;
}
&:hover {
background: $primary-color;
}
}
}
}
.table {
.td,
.th {
border-bottom: $border-width solid $border-color;
}
&,
&.table-striped {
.tbody {
.tr {
&.selected {
background: rgba($bg-color-gray, 0.2) !important;
}
&.active {
background: $bg-color;
}
}
}
}
&.table-hover {
.tbody {
.tr {
&:hover {
background: $bg-color-light-gray;
}
}
}
}
&.table-striped {
.tbody {
.tr:nth-of-type(odd) {
background: $bg-color;
}
}
}
}
#footer {
background: $primary-color;
box-shadow: 0 0 1px 0 #000;
.footer-elements {
.footer-element {
&.footer-link {
&:hover {
background: rgba($color: #fff, $alpha: 0.1);
}
}
}
}
}
}

View File

@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import application from './modules/application.store';
import settings from './modules/settings.store';
import scratchpad from './modules/scratchpad.store';
import connections from './modules/connections.store';
import workspaces from './modules/workspaces.store';
import notifications from './modules/notifications.store';
@@ -18,6 +19,7 @@ export default new Vuex.Store({
modules: {
application,
settings,
scratchpad,
connections,
workspaces,
notifications

View File

@@ -8,18 +8,22 @@ export default {
is_loading: false,
is_new_modal: false,
is_setting_modal: false,
is_scratchpad: false,
selected_setting_tab: 'general',
selected_conection: {},
update_status: 'noupdate', // noupdate, available, checking, nocheck, downloading, downloaded
download_progress: 0
update_status: 'noupdate', // noupdate, available, checking, nocheck, downloading, downloaded, disabled
download_progress: 0,
base_completer: [] // Needed to reset ace editor, due global-only ace completer
},
getters: {
isLoading: state => state.is_loading,
appName: state => state.app_name,
appVersion: state => state.app_version,
getBaseCompleter: state => state.base_completer,
getSelectedConnection: state => state.selected_conection,
isNewModal: state => state.is_new_modal,
isSettingModal: state => state.is_setting_modal,
isScratchpad: state => state.is_scratchpad,
selectedSettingTab: state => state.selected_setting_tab,
getUpdateStatus: state => state.update_status,
getDownloadProgress: state => Number(state.download_progress.toFixed(1))
@@ -28,6 +32,9 @@ export default {
SET_LOADING_STATUS (state, payload) {
state.is_loading = payload;
},
SET_BASE_COMPLETER (state, payload) {
state.base_completer = payload;
},
SHOW_NEW_CONNECTION_MODAL (state) {
state.is_new_modal = true;
},
@@ -41,6 +48,12 @@ export default {
HIDE_SETTING_MODAL (state) {
state.is_setting_modal = false;
},
SHOW_SCRATCHPAD (state) {
state.is_scratchpad = true;
},
HIDE_SCRATCHPAD (state) {
state.is_scratchpad = false;
},
CHANGE_UPDATE_STATUS (state, status) {
state.update_status = status;
},
@@ -52,6 +65,9 @@ export default {
setLoadingStatus ({ commit }, payload) {
commit('SET_LOADING_STATUS', payload);
},
setBaseCompleter ({ commit }, payload) {
commit('SET_BASE_COMPLETER', payload);
},
// Modals
showNewConnModal ({ commit }) {
commit('SHOW_NEW_CONNECTION_MODAL');
@@ -64,6 +80,12 @@ export default {
},
hideSettingModal ({ commit }) {
commit('HIDE_SETTING_MODAL');
},
showScratchpad ({ commit }) {
commit('SHOW_SCRATCHPAD');
},
hideScratchpad ({ commit }) {
commit('HIDE_SCRATCHPAD');
}
}
};

View File

@@ -1,7 +1,13 @@
'use strict';
import Store from 'electron-store';
import crypto from 'crypto';
import Application from '../../ipc-api/Application';
const key = Application.getKey();
const key = Application.getKey() || localStorage.getItem('key');
if (!key)
localStorage.setItem('key', crypto.randomBytes(16).toString('hex'));
else
localStorage.setItem('key', key);
const persistentStore = new Store({
name: 'connections',
@@ -12,7 +18,7 @@ export default {
namespaced: true,
strict: true,
state: {
connections: persistentStore.get('connections') || []
connections: persistentStore.get('connections', [])
},
getters: {
getConnections: state => state.connections,

View File

@@ -0,0 +1,25 @@
'use strict';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'notes' });
export default {
namespaced: true,
strict: true,
state: {
notes: persistentStore.get('notes', '# HOW TO SUPPORT ANTARES\n\n- [ ] Leave a star to Antares [GitHub repo](https://github.com/Fabio286/antares)\n- [ ] Send feedbacks and advices\n- [ ] Report for bugs\n- [ ] If you enjoy, share Antares with friends\n\n# ABOUT SCRATCHPAD\n\nThis is a scratchpad where you can save your **personal notes**. It supports `markdown` format, but you are free to use plain text.\nThis content is just a placeholder, feel free to clear it to make space for your notes.\n')
},
getters: {
getNotes: state => state.notes
},
mutations: {
SET_NOTES (state, notes) {
state.notes = notes;
persistentStore.set('notes', state.notes);
}
},
actions: {
changeNotes ({ commit }, notes) {
commit('SET_NOTES', notes);
}
}
};

View File

@@ -7,16 +7,18 @@ export default {
namespaced: true,
strict: true,
state: {
locale: persistentStore.get('locale') || 'en-US',
explorebar_size: persistentStore.get('explorebar_size') || null,
notifications_timeout: persistentStore.get('notifications_timeout') || 5,
auto_complete: persistentStore.get('auto_complete') || true,
line_wrap: persistentStore.get('line_wrap') || true,
application_theme: persistentStore.get('application_theme') || 'dark',
editor_theme: persistentStore.get('editor_theme') || 'twilight'
locale: persistentStore.get('locale', 'en-US'),
allow_prerelease: persistentStore.get('allow_prerelease', true),
explorebar_size: persistentStore.get('explorebar_size', null),
notifications_timeout: persistentStore.get('notifications_timeout', 5),
auto_complete: persistentStore.get('auto_complete', true),
line_wrap: persistentStore.get('line_wrap', true),
application_theme: persistentStore.get('application_theme', 'dark'),
editor_theme: persistentStore.get('editor_theme', 'twilight')
},
getters: {
getLocale: state => state.locale,
getAllowPrerelease: state => state.allow_prerelease,
getExplorebarSize: state => state.explorebar_size,
getNotificationsTimeout: state => state.notifications_timeout,
getAutoComplete: state => state.auto_complete,
@@ -30,6 +32,10 @@ export default {
i18n.locale = locale;
persistentStore.set('locale', state.locale);
},
SET_ALLOW_PRERELEASE (state, allow) {
state.allow_prerelease = allow;
persistentStore.set('allow_prerelease', state.allow_prerelease);
},
SET_NOTIFICATIONS_TIMEOUT (state, timeout) {
state.notifications_timeout = timeout;
persistentStore.set('notifications_timeout', state.notifications_timeout);
@@ -46,14 +52,22 @@ export default {
state.explorebar_size = size;
persistentStore.set('explorebar_size', state.explorebar_size);
},
SET_APPLICATION_THEME (state, theme) {
state.application_theme = theme;
persistentStore.set('application_theme', state.application_theme);
},
SET_EDITOR_THEME (state, theme) {
state.editor_theme = theme;
persistentStore.set('editor_theme', state.editor_theme);
}
},
actions: {
changeLocale ({ commit }, locale) {
commit('SET_LOCALE', locale);
},
changeAllowPrerelease ({ commit }, allow) {
commit('SET_ALLOW_PRERELEASE', allow);
},
updateNotificationsTimeout ({ commit }, timeout) {
commit('SET_NOTIFICATIONS_TIMEOUT', timeout);
},
@@ -66,6 +80,9 @@ export default {
changeLineWrap ({ commit }, val) {
commit('SET_LINE_WRAP', val);
},
changeApplicationTheme ({ commit }, theme) {
commit('SET_APPLICATION_THEME', theme);
},
changeEditorTheme ({ commit }, theme) {
commit('SET_EDITOR_THEME', theme);
}

View File

@@ -1,6 +1,6 @@
'use strict';
import Connection from '@/ipc-api/Connection';
import Database from '@/ipc-api/Database';
import Schema from '@/ipc-api/Schema';
import Users from '@/ipc-api/Users';
import { uidGen } from 'common/libs/uidGen';
const tabIndex = [];
@@ -43,6 +43,9 @@ export default {
getLoadedSchemas: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).loaded_schemas;
},
getSearchTerm: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).search_term;
},
isUnsavedDiscardModal: state => {
return state.is_unsaved_discard_modal;
}
@@ -52,7 +55,7 @@ export default {
state.selected_workspace = uid;
},
ADD_CONNECTED (state, payload) {
const { uid, client, dataTypes, indexTypes, structure, version } = payload;
const { uid, client, dataTypes, indexTypes, customizations, structure, version } = payload;
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
@@ -60,6 +63,7 @@ export default {
client,
dataTypes,
indexTypes,
customizations,
structure,
connected: true,
version
@@ -141,7 +145,15 @@ export default {
}
: workspace);
},
NEW_TAB (state, { uid, tab }) {
SET_SEARCH_TERM (state, { uid, term }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
search_term: term
}
: workspace);
},
NEW_TAB (state, { uid, tab, content, autorun }) {
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
const newTab = {
uid: tab,
@@ -149,7 +161,9 @@ export default {
selected: false,
type: 'query',
fields: [],
keyUsage: []
keyUsage: [],
content: content || '',
autorun: !!autorun
};
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
@@ -240,16 +254,23 @@ export default {
else {
let dataTypes = [];
let indexTypes = [];
let customizations = {};
switch (connection.client) {
case 'mysql':
case 'maria':
dataTypes = require('common/data-types/mysql');
indexTypes = require('common/index-types/mysql');
customizations = require('common/customizations/mysql');
break;
case 'pg':
dataTypes = require('common/data-types/postgresql');
indexTypes = require('common/index-types/postgresql');
customizations = require('common/customizations/postgresql');
break;
}
const { status, response: version } = await Database.getVersion(connection.uid);
const { status, response: version } = await Schema.getVersion(connection.uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: version }, { root: true });
@@ -272,6 +293,7 @@ export default {
client: connection.client,
dataTypes,
indexTypes,
customizations,
structure: response,
version
});
@@ -287,7 +309,7 @@ export default {
},
async refreshStructure ({ dispatch, commit, getters }, uid) {
try {
const { status, response } = await Database.getStructure({ uid, schemas: getters.getLoadedSchemas(uid) });
const { status, response } = await Schema.getStructure({ uid, schemas: getters.getLoadedSchemas(uid) });
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
@@ -300,7 +322,7 @@ export default {
},
async refreshSchema ({ dispatch, commit }, { uid, schema }) {
try {
const { status, response } = await Database.getStructure({ uid, schemas: new Set([schema]) });
const { status, response } = await Schema.getStructure({ uid, schemas: new Set([schema]) });
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -312,7 +334,7 @@ export default {
},
async refreshCollations ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getCollations(uid);
const { status, response } = await Schema.getCollations(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -324,7 +346,7 @@ export default {
},
async refreshVariables ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getVariables(uid);
const { status, response } = await Schema.getVariables(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -336,7 +358,7 @@ export default {
},
async refreshEngines ({ dispatch, commit }, uid) {
try {
const { status, response } = await Database.getEngines(uid);
const { status, response } = await Schema.getEngines(uid);
if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true });
else
@@ -368,6 +390,7 @@ export default {
uid,
connected: false,
selected_tab: 0,
search_term: '',
tabs: [],
structure: {},
variables: [],
@@ -380,7 +403,7 @@ export default {
commit('ADD_WORKSPACE', workspace);
if (getters.getWorkspace(uid).tabs.length < 3)
dispatch('newTab', uid);
dispatch('newTab', { uid });
dispatch('setUnsavedChanges', false);
},
@@ -407,7 +430,7 @@ export default {
if (lastBreadcrumbs.schema === payload.schema && hasLastChildren && !hasChildren) return;
if (lastBreadcrumbs.schema !== payload.schema)
Database.useSchema({ uid: getters.getSelected, schema: payload.schema });
Schema.useSchema({ uid: getters.getSelected, schema: payload.schema });
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...breadcrumbsObj, ...payload } });
lastBreadcrumbs = { ...breadcrumbsObj, ...payload };
@@ -415,10 +438,13 @@ export default {
if (payload.schema)
commit('ADD_LOADED_SCHEMA', { uid: getters.getSelected, schema: payload.schema });
},
newTab ({ commit }, uid) {
setSearchTerm ({ commit, getters }, term) {
commit('SET_SEARCH_TERM', { uid: getters.getSelected, term });
},
newTab ({ commit }, { uid, content, autorun }) {
const tab = uidGen('T');
commit('NEW_TAB', { uid, tab });
commit('NEW_TAB', { uid, tab, content, autorun });
commit('SELECT_TAB', { uid, tab });
},
removeTab ({ commit }, payload) {

View File

@@ -13,6 +13,9 @@ export default store => {
ipcRenderer.on('check-failed', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'nocheck');
});
ipcRenderer.on('no-auto-update', () => {
store.commit('application/CHANGE_UPDATE_STATUS', 'disabled');
});
ipcRenderer.on('download-progress', (event, data) => {
store.commit('application/CHANGE_UPDATE_STATUS', 'downloading');
store.commit('application/CHANGE_PROGRESS_PERCENTAGE', data.percent);