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

Compare commits

...

77 Commits

Author SHA1 Message Date
c8594c0549 chore(release): 0.3.7 2021-10-08 09:13:23 +02:00
7359c3b5bd Merge pull request #107 from Fabio286/dependabot/npm_and_yarn/ssh2-promise-1.0.2
build(deps): bump ssh2-promise from 0.2.0 to 1.0.2
2021-10-07 15:27:40 +02:00
4195b8416f Create CONTRIBUTING.md 2021-10-07 15:15:12 +02:00
9407a29922 feat: support to SSH private keys with passphrase, closes #118 2021-10-07 14:58:31 +02:00
2fcd080bd4 fix(PostgreSQL): issue with uppercase characters in table and field names, closes #116 2021-10-06 12:08:37 +02:00
26446fb7ed fix: closing ask credential modal during a connection doesn't stops loading, closes #114 2021-10-05 18:20:38 +02:00
2480c76a08 chore: update README.md 2021-10-05 10:53:45 +02:00
1f0ec57789 Merge pull request #115 from IsamuSugi/Japanese-translation
Add new language: Japanese
2021-10-05 10:49:47 +02:00
isamu
76e5849c78 Add new language: Japanese 2021-10-05 17:22:07 +09:00
db1641b74f fix(UI): window reload pressing enter in schema creation modal, closes #113 2021-10-05 09:16:45 +02:00
165c54f663 Merge pull request #111 from datlechin/master
Add new language: Tiếng Việt
2021-10-05 09:04:19 +02:00
Ngo Quoc Dat
a5ca3ea204 Add new language: Tiếng Việt 2021-10-04 18:30:44 +07:00
dependabot[bot]
add95292ad build(deps): bump ssh2-promise from 0.2.0 to 1.0.2
Bumps [ssh2-promise](https://github.com/sanketbajoria/ssh2-promise) from 0.2.0 to 1.0.2.
- [Release notes](https://github.com/sanketbajoria/ssh2-promise/releases)
- [Commits](https://github.com/sanketbajoria/ssh2-promise/commits)

---
updated-dependencies:
- dependency-name: ssh2-promise
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-01 13:52:48 +00:00
7a63608f54 feat(UI): auto detect system theme as default app theme 2021-10-01 15:48:02 +02:00
7ea7b369ab chore: update dependencies 2021-10-01 15:33:10 +02:00
258fffa958 chore: update README.md 2021-09-30 09:05:40 +02:00
0849c5131f Merge pull request #106 from Fabio286/dependabot/npm_and_yarn/electron-15.0.0
build(deps-dev): bump electron from 14.0.1 to 15.0.0
2021-09-28 09:19:16 +02:00
dependabot[bot]
cce59d0ca8 build(deps-dev): bump electron from 14.0.1 to 15.0.0
Bumps [electron](https://github.com/electron/electron) from 14.0.1 to 15.0.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v14.0.1...v15.0.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-27 19:06:30 +00:00
94a53fec6c chore(release): 0.3.6 2021-09-26 11:39:19 +02:00
13aa47cd44 feat: processes list exportation 2021-09-26 11:36:42 +02:00
85f625daf7 feat: copy cell/row or kill connections on context menu from processes list 2021-09-26 11:19:48 +02:00
7de3bb9346 refactor(core): compatibility with electron/remote 2.x 2021-09-21 11:16:07 +02:00
e43a0ba0b4 Merge pull request #102 from Fabio286/dependabot/npm_and_yarn/electron/remote-2.0.1
build(deps): bump @electron/remote from 1.2.2 to 2.0.1
2021-09-21 10:03:35 +02:00
dependabot[bot]
638cd4bfb7 build(deps): bump @electron/remote from 1.2.2 to 2.0.1
Bumps [@electron/remote](https://github.com/electron/remote) from 1.2.2 to 2.0.1.
- [Release notes](https://github.com/electron/remote/releases)
- [Changelog](https://github.com/electron/remote/blob/main/.releaserc.json)
- [Commits](https://github.com/electron/remote/compare/v1.2.2...v2.0.1)

---
updated-dependencies:
- dependency-name: "@electron/remote"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-20 19:06:45 +00:00
3959333662 feat: workspace query history 2021-09-17 18:32:28 +02:00
abd46aa322 perf(core): better communication of internal exceptions 2021-09-15 15:31:57 +02:00
d4888ad8fb fix: adding a connection default values not change when switching clients, closes #101 2021-09-15 11:08:00 +02:00
6bfc229b77 chore(release): 0.3.5 2021-09-13 15:03:02 +02:00
d31b051f4b chore: update dependencies 2021-09-13 15:02:28 +02:00
95b60df8fc fix(MySQL): connections stuck at startup if 5 or more tabs are restored 2021-09-13 10:03:27 +02:00
ed5189fdc1 chore(release): 0.3.4 2021-09-12 11:59:01 +02:00
265ed66d25 feat: start search when typing with focus on the left bar 2021-09-12 11:55:16 +02:00
09c07acd5c feat: new create trigger function tabs 2021-09-11 10:24:21 +02:00
3c5a69adc9 feat: new create scheduler tabs 2021-09-10 18:23:32 +02:00
0203f69e95 feat: new create function tabs 2021-09-07 18:20:45 +02:00
9a2498862c Merge pull request #97 from Fabio286/dependabot/npm_and_yarn/electron-14.0.0
build(deps-dev): bump electron from 13.3.0 to 14.0.0
2021-09-07 09:07:29 +02:00
dependabot[bot]
3a26d4f509 build(deps-dev): bump electron from 13.3.0 to 14.0.0
Bumps [electron](https://github.com/electron/electron) from 13.3.0 to 14.0.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v13.3.0...v14.0.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-06 19:44:11 +00:00
ce08cbd8b5 Merge pull request #98 from Fabio286/dependabot/npm_and_yarn/mdi/font-6.1.95
build(deps): bump @mdi/font from 5.9.55 to 6.1.95
2021-09-06 21:41:47 +02:00
dependabot[bot]
d91ffcccca build(deps): bump @mdi/font from 5.9.55 to 6.1.95
Bumps [@mdi/font](https://github.com/Templarian/MaterialDesign-Webfont) from 5.9.55 to 6.1.95.
- [Release notes](https://github.com/Templarian/MaterialDesign-Webfont/releases)
- [Commits](https://github.com/Templarian/MaterialDesign-Webfont/compare/v5.9.55...v6.1.95)

---
updated-dependencies:
- dependency-name: "@mdi/font"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-06 19:06:42 +00:00
6115eb9409 refactor: improvements in explorebar events on tables 2021-09-06 18:02:54 +02:00
3fd26a0523 feat: new create routine tabs 2021-09-06 17:29:34 +02:00
e217d5181b feat: new create trigger tabs 2021-09-02 18:08:23 +02:00
dcf368b350 chore: update dependencies 2021-09-01 18:17:11 +02:00
21e3e79ddf fix(UI): context menu of tables cut if close to bottom edge 2021-08-31 19:58:20 +02:00
2918c3cb92 chore(release): 0.3.3 2021-08-22 10:41:48 +02:00
3ad190b18c Merge branch 'master' of https://github.com/Fabio286/antares 2021-08-22 10:37:27 +02:00
d5b2bde2ea fix(UI): no round borders on left of file upload inputs 2021-08-22 10:37:23 +02:00
a42348ef5c chore: deletion of wrong file name case component 2021-08-20 13:42:29 +02:00
8b93c49778 feat: new create view tabs 2021-08-18 17:28:41 +02:00
0842e00098 feat: new table empty state 2021-08-17 18:54:23 +02:00
6cef02bebb Merge pull request #93 from Fabio286/dependabot/npm_and_yarn/marked-3.0.0
build(deps): bump marked from 2.1.3 to 3.0.0
2021-08-17 12:04:22 +02:00
dependabot[bot]
334c7a31d2 build(deps): bump marked from 2.1.3 to 3.0.0
Bumps [marked](https://github.com/markedjs/marked) from 2.1.3 to 3.0.0.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/release.config.js)
- [Commits](https://github.com/markedjs/marked/compare/v2.1.3...v3.0.0)

---
updated-dependencies:
- dependency-name: marked
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-16 19:05:09 +00:00
bc82289d54 perf(UI): visual improvements of tables 2021-08-16 19:44:55 +02:00
c9fa941578 feat: new create table tabs 2021-08-13 16:50:59 +02:00
4048df3c7b fix(MySQL): editing a view causes error for missing database in some conditions 2021-08-12 10:29:13 +02:00
198368605b perf(UI): improved view setting tab 2021-08-12 10:07:31 +02:00
8f0ac26b69 refactor: better component names 2021-08-12 09:54:13 +02:00
b35fc5b78b fix(UI): multiple temp tabs opened switching to tables from other elements 2021-08-11 18:04:14 +02:00
622b519cbb fix: table options not loaded on restored setting tabs at startup 2021-08-11 16:16:58 +02:00
71e2c911ae perf(UI): element options in setting tabs accessible directly 2021-08-10 18:12:13 +02:00
756d49b259 perf(UI): primary app color on selected text backgrouns 2021-08-10 13:14:48 +02:00
14c64c537c chore(release): 0.3.2 2021-08-06 14:44:45 +02:00
97b3563e25 chore: update dependencies 2021-08-06 14:38:25 +02:00
e834fe31ac feat(UI): automatic scroll on selected tab 2021-08-05 13:44:48 +02:00
065de3a0a2 feat(UI): query tab name based on content 2021-08-05 13:30:33 +02:00
1573de5b1f feat(UI): button to clear sidebar search input 2021-08-05 12:47:24 +02:00
04fc1bbee0 feat(UI): automatic scroll to selected tab element in left bar 2021-08-05 12:09:54 +02:00
dea378014d perf: approximate table total updated on table refresh 2021-08-04 15:52:26 +02:00
3abff36136 feat: contextual menu option to duplicate table fields 2021-08-04 09:59:50 +02:00
70354aa828 feat(UI): shortcuts info on empty query tab 2021-08-03 17:59:15 +02:00
372049ae64 perf(UI): loading animation on tables and table context menu improvements 2021-08-03 15:43:13 +02:00
5d271be062 chore: update README.md 2021-08-02 19:26:56 +02:00
07ee1ae828 fix: tab selected when clicking closing cross 2021-07-29 16:45:28 +02:00
cbe0e2980a perf: update italian translation 2021-07-27 17:31:51 +02:00
0fba50c6ec chore: update README.md 2021-07-27 17:18:11 +02:00
420255cdd4 chore(release): 0.3.1 2021-07-27 10:27:02 +02:00
a8a47ed5f7 fix(UI): tabs or explorebar elements selected with mouse wheel or right button 2021-07-23 22:41:53 +02:00
94 changed files with 6030 additions and 2745 deletions

2
.gitignore vendored
View File

@@ -4,6 +4,6 @@ node_modules/
thumbs.db
.idea/
.vscode
TODO.md
NOTES.md
*.txt
package-lock.json

View File

@@ -2,6 +2,120 @@
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.3.7](https://github.com/Fabio286/antares/compare/v0.3.6...v0.3.7) (2021-10-08)
### Features
* support to SSH private keys with passphrase, closes [#118](https://github.com/Fabio286/antares/issues/118) ([9407a29](https://github.com/Fabio286/antares/commit/9407a29922812ab6aa3cf67569ba2f509433657c))
* **UI:** auto detect system theme as default app theme ([7a63608](https://github.com/Fabio286/antares/commit/7a63608f54e387d45e655855666041f5602b54b1))
### Bug Fixes
* closing ask credential modal during a connection doesn't stops loading, closes [#114](https://github.com/Fabio286/antares/issues/114) ([26446fb](https://github.com/Fabio286/antares/commit/26446fb7ed04216283736072d442786350252dbb))
* **PostgreSQL:** issue with uppercase characters in table and field names, closes [#116](https://github.com/Fabio286/antares/issues/116) ([2fcd080](https://github.com/Fabio286/antares/commit/2fcd080bd47367a21590ea5a754410a975959bdd))
* **UI:** window reload pressing enter in schema creation modal, closes [#113](https://github.com/Fabio286/antares/issues/113) ([db1641b](https://github.com/Fabio286/antares/commit/db1641b74fcd218f1f1d24163cba70b024cc6bd7))
### [0.3.6](https://github.com/Fabio286/antares/compare/v0.3.5...v0.3.6) (2021-09-26)
### Features
* copy cell/row or kill connections on context menu from processes list ([85f625d](https://github.com/Fabio286/antares/commit/85f625daf7026815dac6223a29c5a6479830edbb))
* processes list exportation ([13aa47c](https://github.com/Fabio286/antares/commit/13aa47cd4441aa47c93038dbd91d6a0e54f6a60c))
* workspace query history ([3959333](https://github.com/Fabio286/antares/commit/39593336626e6d9f3d3b65d2a4081388900e37d6))
### Bug Fixes
* adding a connection default values not change when switching clients, closes [#101](https://github.com/Fabio286/antares/issues/101) ([d4888ad](https://github.com/Fabio286/antares/commit/d4888ad8fba3c8e8ec2d6b6d9a78bb212d83eeed))
### Improvements
* **core:** better communication of internal exceptions ([abd46aa](https://github.com/Fabio286/antares/commit/abd46aa32256f822e52eaac2fc698da378b8163f))
### [0.3.5](https://github.com/Fabio286/antares/compare/v0.3.4...v0.3.5) (2021-09-13)
### Bug Fixes
* **MySQL:** connections stuck at startup if 5 or more tabs are restored ([95b60df](https://github.com/Fabio286/antares/commit/95b60df8fc634b96a4c2c5c48dc6b10848888978))
### [0.3.4](https://github.com/Fabio286/antares/compare/v0.3.3...v0.3.4) (2021-09-12)
### Features
* new create function tabs ([0203f69](https://github.com/Fabio286/antares/commit/0203f69e95093d25a7ef3e66df1c70f76edcedf2))
* new create routine tabs ([3fd26a0](https://github.com/Fabio286/antares/commit/3fd26a05238368ae375197c79d42162a5910bb07))
* new create scheduler tabs ([3c5a69a](https://github.com/Fabio286/antares/commit/3c5a69adc9cecdc4b8f46097676005f2a60f06cf))
* new create trigger function tabs ([09c07ac](https://github.com/Fabio286/antares/commit/09c07acd5c2a6ed2b75640dbb83e782ed432bc30))
* new create trigger tabs ([e217d51](https://github.com/Fabio286/antares/commit/e217d5181b37ec6304151120b4a2aba9455c6a84))
* start search when typing with focus on the left bar ([265ed66](https://github.com/Fabio286/antares/commit/265ed66d25d35be99ed0a6b1668dab9f246ed71e))
### Bug Fixes
* **UI:** context menu of tables cut if close to bottom edge ([21e3e79](https://github.com/Fabio286/antares/commit/21e3e79ddf9e292bcfc5881b8fa76a1dc58b207c))
### [0.3.3](https://github.com/Fabio286/antares/compare/v0.3.2...v0.3.3) (2021-08-22)
### Features
* new create table tabs ([c9fa941](https://github.com/Fabio286/antares/commit/c9fa9415787c3953043db5876a99b3664c69c071))
* new create view tabs ([8b93c49](https://github.com/Fabio286/antares/commit/8b93c497784ea431f9747c5afb53f6ef075ea9d6))
* new table empty state ([0842e00](https://github.com/Fabio286/antares/commit/0842e00098ba420412937aa52276ee33bda53693))
### Bug Fixes
* **MySQL:** editing a view causes error for missing database in some conditions ([4048df3](https://github.com/Fabio286/antares/commit/4048df3c7bc2d42a60f7a57c9a4b8b5b445fcd43))
* table options not loaded on restored setting tabs at startup ([622b519](https://github.com/Fabio286/antares/commit/622b519cbb5fbe4e38a4baffb8eab169b21eed21))
* **UI:** multiple temp tabs opened switching to tables from other elements ([b35fc5b](https://github.com/Fabio286/antares/commit/b35fc5b78bdbeff1422ef088441b17c8b0df663c))
* **UI:** no round borders on left of file upload inputs ([d5b2bde](https://github.com/Fabio286/antares/commit/d5b2bde2eaf8ff3e14f49cc26acdcb201b4cb15c))
### Improvements
* **UI:** element options in setting tabs accessible directly ([71e2c91](https://github.com/Fabio286/antares/commit/71e2c911ae23e86543b2af1fa885981ff271777d))
* **UI:** improved view setting tab ([1983686](https://github.com/Fabio286/antares/commit/198368605b084bd58fd6f7ca0b19895ba23a45e6))
* **UI:** primary app color on selected text backgrouns ([756d49b](https://github.com/Fabio286/antares/commit/756d49b2596dea58376c6afa8e0bad0cd62b146c))
* **UI:** visual improvements of tables ([bc82289](https://github.com/Fabio286/antares/commit/bc82289d54550a93300fe66d7a660aa70db2fd23))
### [0.3.2](https://github.com/Fabio286/antares/compare/v0.3.1...v0.3.2) (2021-08-06)
### Features
* contextual menu option to duplicate table fields ([3abff36](https://github.com/Fabio286/antares/commit/3abff3613618ddc86e1d6c898f83bcb360e8e5d9))
* **UI:** automatic scroll on selected tab ([e834fe3](https://github.com/Fabio286/antares/commit/e834fe31ac8b73f249e3ad8654e97da834b28cfe))
* **UI:** automatic scroll to selected tab element in left bar ([04fc1bb](https://github.com/Fabio286/antares/commit/04fc1bbee0cdc3b02288ff5d3b2c402633cae1f1))
* **UI:** button to clear sidebar search input ([1573de5](https://github.com/Fabio286/antares/commit/1573de5b1f545328523aaaaac541f5a8046617be))
* **UI:** query tab name based on content ([065de3a](https://github.com/Fabio286/antares/commit/065de3a0a28a9e6667df4e41ac1c5fa5561d2171))
* **UI:** shortcuts info on empty query tab ([70354aa](https://github.com/Fabio286/antares/commit/70354aa828121004f70e91d65cb9f0a4811dce57))
### Bug Fixes
* tab selected when clicking closing cross ([07ee1ae](https://github.com/Fabio286/antares/commit/07ee1ae828bb5f9971a263f2e39e73c760fed50a))
### Improvements
* approximate table total updated on table refresh ([dea3780](https://github.com/Fabio286/antares/commit/dea378014dd45bf90f51a599b4a801e0bc22059f))
* **UI:** loading animation on tables and table context menu improvements ([372049a](https://github.com/Fabio286/antares/commit/372049ae64232e8e61a974edc8be5b319c5c0811))
* update italian translation ([cbe0e29](https://github.com/Fabio286/antares/commit/cbe0e2980a9d4562941aec37a57cc936a46f41d8))
### [0.3.1](https://github.com/Fabio286/antares/compare/v0.3.0...v0.3.1) (2021-07-27)
### Bug Fixes
* **UI:** tabs or explorebar elements selected with mouse wheel or right button ([a8a47ed](https://github.com/Fabio286/antares/commit/a8a47ed5f7d5d8cbbdd6c33ef8307d9dce5b193b))
## [0.3.0](https://github.com/Fabio286/antares/compare/v0.2.1...v0.3.0) (2021-07-23)

120
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,120 @@
# Contributors Guide
Antares SQL is an application based on [Electron.js](https://www.electronjs.org/) that uses [Vue.js](https://vuejs.org/) and [Spectre.css](https://picturepan2.github.io/spectre/) as frontend frameworks.
For the build process it takes advantage of [electron-builder](https://www.electron.build/).
This application uses [Vuex](https://vuex.vuejs.org/) as application state manager and [electron-store](https://github.com/sindresorhus/electron-store) to save the various settings on disc.
This guide aims to provide useful information and guidelines to everyone wants to contribute with this open-source project.
For every other question related to this project please [contact me](https://github.com/Fabio286).
## Project Structure
The main files of the application are located inside `src` folder and are groupped in three subfolders.
### `common`
This folder contains small libraries, classes and objects. The purpose of `common` folder is to group together utilities used by **renderer** and **main** processes.
Noteworthy is the `customizations` folder that contains clients related customizations. Those settings are merged with `default.js` that lists every option.
Client related customizations are stored on Vuex and can be accessed by `customizations` property of current workspace object, or importing `common/customizations`.
An use case of customizations object can be the following:
```js
computed: {
defaultEngine () {
if (this.workspace.customizations.engines)
return this.workspace.engines.find(engine => engine.isDefault).name;
return '';
}
}
```
In this case the computed property `defaultEngine` returns the default engine for MySQL client, or an empty string with PostgreSQL that doesn't have engines.
Customization properties are also useful **if some features are ready for one client but not others**.
### `main`
Inside this folder are located all files required by main process.
`ipc-handlers` subfolder includes all IPC handlers for events sent from renderer process.
`libs` subfolder includes classes related to clients and **query and connection logics**.
**Everything above client's class level should be "client agnostic"** with a neutral and uniformed api interface
### `renderer`
In this folder is located the structure of Vue frontend application.
## Build
The command to build Antares SQL locally is `npm run build:local`.
`build` command (without `:local`) is used exclusively by the GitHub Action.
## Conventions
### Electron
- **kebab-case** for IPC event names.
### Vue
- **PascalCase** for file names (with .vue extension) and including components inside others (`<MyComponent/>`).
- "**Base**" prefix for [base component names](https://vuejs.org/v2/style-guide/#Base-component-names-strongly-recommended).
- "**The**" prefix for [single-instance component names](https://vuejs.org/v2/style-guide/#Single-instance-component-names-strongly-recommended).
- [Tightly coupled component names ](https://vuejs.org/v2/style-guide/#Tightly-coupled-component-names-strongly-recommended).
- [Order of words in component names](https://vuejs.org/v2/style-guide/#Order-of-words-in-component-names-strongly-recommended).
- **kebab-case** in templates for property and event names.
### Vuex
- **snake_case** for state names.
- **camelCase** for getter and action names.
- **SNAKE_CASE (all caps)** for mutation names.
### Code Style
The project includes [ESlint](https://eslint.org/) and [StyleLint](https://stylelint.io/) config files with style rules. I recommend to set the lint on-save option in your code editor.
Alternatively you can launch following commands to lint the project.
Check if all the style rules have been followed:
```console
npm run lint
```
Apply style rules globally if possible:
```console
npm run lint:fix
```
### Commits
The commit style adopted for this project is [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
Basicly it's important to have **single scoped commits with a prefix** that follows this style because Antares SQL uses [standard-version](https://github.com/conventional-changelog/standard-version) to generate new releases and [CHANGELOG.md](https://github.com/Fabio286/antares/blob/master/CHANGELOG.md) file to track all notable changes.
For Visual Studio Code users may be useful [Conventional Commits](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits) extension.
## Debug
**Dev mode**:
```console
npm run dev
```
**Visual Studio Code:**
``` json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Electron in debugger",
"autoAttachChildProcesses": true,
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-webpack",
"runtimeArgs": [
"dev"
],
"env": {},
"console": "integratedTerminal",
}
]
}
```

View File

@@ -4,10 +4,10 @@
# Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Ffabio286%2Fantares%2Fbadge&style=flat)](https://actions-badge.atrox.dev/fabio286/antares/goto) ![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) [![Twitter Follow](https://img.shields.io/twitter/follow/AntaresSQL?style=social)](https://twitter.com/AntaresSQL) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Ffabio286%2Fantares%2Fbadge&style=flat)](https://actions-badge.atrox.dev/fabio286/antares/goto) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares) [![Twitter Follow](https://img.shields.io/twitter/follow/AntaresSQL?style=social)](https://twitter.com/AntaresSQL) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
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.
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL.
At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
@@ -17,38 +17,50 @@ We are actively working on it, hoping to provide new cool features, improvements
👁 To stay tuned for new releases [follow Antares SQL](https://twitter.com/AntaresSQL) on Twitter.
🌟 Don't forget to **leave a star** if you appreciate this project.
## Current key features
- Multiple database connections at same time.
- Database management (add/edit/delete).
- Full tables management, including indexes and foreign keys.
- Views, triggers, stored routines, functions and schedulers management (add/edit/delete).
- A modern and friendly tab system; keep open every kind of tab you need in your workspace.
- Fake table data filler to generate tons of data for test purpose.
- Query suggestions and auto complete.
- Query history: search through the last 1000 queries.
- SSH tunnel support.
- Dark and light theme.
- Editor themes.
- Scratchpad.
- Secure password storage.
## Philosophy
Why are we 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.
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenu; productivity comes first.
## Installation
Based on your operating system you can have one or more distribution formats to choose based on your preferences.
Since Antares SQL is a free software we haven't a budget to spend in annual licenses or certificates. This can result that on some platforms you need some additional passages to install this app.
### Linux
On Linux you can simply download and run `.AppImage` distributions, install from Snap Store or from AUR.
### Windows
On Windows you can choose between Microsoft Store and download `.exe` distribution. The latter lacks of a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt.
### MacOS
On macOS you can run `.dmg` distribution following [this guide](https://support.apple.com/guide/mac-help/mh40616/mac) to install apps from unknown developers.
## 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 AUR](https://raw.githubusercontent.com/Fabio286/antares/3e00c4bae6e036300c752c1a40c5a038fea9c169/docs/aur-badge.svg)](https://aur.archlinux.org/packages/antares-sql/) [![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
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1)
## Current main features
- Multiple database connections at same time.
- Database management (add/edit/delete).
- Full tables management, including indexes and foreign keys.
- Views, triggers, stored routines, functions and schedulers management (add/edit/delete).
- Fake table data filler.
- Run queries on multiple tabs.
- Query suggestions and auto complete.
- SSH tunnel support.
- Dark and light theme.
- Scratchpad.
- Multi language.
- Secure password storage.
## Coming soon
This is a roadmap with major features will come in near future.
@@ -56,10 +68,10 @@ This is a roadmap with major features will come in near future.
- Support for other databases.
- Database tools.
- Users management (add/edit/delete).
- Query history and bookmarks.
- More context menu shortcuts.
- More keyboard shortcuts.
- Import/export and migration.
- Apple Silicon distribution
## Currently supported
@@ -78,7 +90,7 @@ This is a roadmap with major features will come in near future.
- [x] Windows
- [x] Linux
- [x] MacOS (not tested due lack of hardware)
- [x] MacOS
#### • ARM
@@ -86,15 +98,19 @@ This is a roadmap with major features will come in near future.
- [x] Linux
- [ ] MacOS
## How to contribute
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1)
## Translations
**Italian Translation** / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
**Arabic Translation** / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
**Spanish Translation** / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
**French Translation** / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
**Italian** / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
**Arabic** (needs updates) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
**Spanish** (needs updates) / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
**French** (needs updates) / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
**Portugues (Brasil)** / [Daniel Eduardo](https://github.com/daeleduardo) [[#54](https://github.com/Fabio286/antares/pull/54)]
**Deutsch (Deutschland)** / [Christian Ratz](https://github.com/digitalgopnik) [[#74](https://github.com/Fabio286/antares/pull/74)]
## Reviews
<a target="_blank" href="https://www.softx64.com/windows/antares-sql-client.html" title="Antares SQL Client review"><img src="https://www.softx64.com/softx64-review.png" alt="Antares SQL Client review" /></a>
**Deutsch (Deutschland)** / [Christian Ratz](https://github.com/digitalgopnik) [[#74](https://github.com/Fabio286/antares/pull/74)]
**Vietnamese** / [Ngô Quốc Đạt](https://github.com/datlechin) [[#111](https://github.com/Fabio286/antares/pull/111)]
**Japanese** / [Isamu Sugiura](https://github.com/IsamuSugi) [[#115](https://github.com/Fabio286/antares/pull/115)]

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.3.0",
"version": "0.3.7",
"description": "A cross-platform easy to use SQL client.",
"license": "MIT",
"repository": "https://github.com/Fabio286/antares.git",
@@ -87,47 +87,47 @@
}
},
"dependencies": {
"@electron/remote": "^1.2.0",
"@mdi/font": "^5.9.55",
"ace-builds": "^1.4.12",
"electron-log": "^4.3.5",
"electron-store": "^8.0.0",
"@electron/remote": "^2.0.1",
"@mdi/font": "^6.1.95",
"ace-builds": "^1.4.13",
"electron-log": "^4.4.1",
"electron-store": "^8.0.1",
"electron-updater": "^4.3.9",
"faker": "^5.5.3",
"marked": "^2.1.1",
"marked": "^3.0.4",
"moment": "^2.29.1",
"mysql2": "^2.2.5",
"pg": "^8.5.1",
"mysql2": "^2.3.0",
"pg": "^8.7.1",
"pgsql-ast-parser": "^7.2.1",
"source-map-support": "^0.5.16",
"source-map-support": "^0.5.20",
"spectre.css": "^0.5.9",
"sql-formatter": "^4.0.2",
"ssh2-promise": "^0.2.0",
"v-mask": "^2.2.4",
"vue-i18n": "^8.24.4",
"ssh2-promise": "^1.0.2",
"v-mask": "^2.3.0",
"vue-i18n": "^8.26.5",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/eslint-parser": "^7.14.5",
"@babel/eslint-parser": "^7.15.7",
"cross-env": "^7.0.2",
"electron": "^13.1.2",
"electron": "^15.0.0",
"electron-builder": "^22.11.7",
"electron-devtools-installer": "^3.2.0",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^7.29.0",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.11.1",
"sass": "^1.35.1",
"eslint-plugin-vue": "^7.18.0",
"sass": "^1.42.1",
"sass-loader": "^10.2.0",
"standard-version": "^9.3.0",
"standard-version": "^9.3.1",
"stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0",
"stylelint-scss": "^3.20.1",
"stylelint-scss": "^3.21.0",
"vue": "^2.6.14",
"vue-template-compiler": "^2.6.14",
"webpack": "^4.46.0"

View File

@@ -44,6 +44,7 @@ module.exports = {
unsigned: false,
nullable: false,
zerofill: false,
tableOptions: false,
autoIncrement: false,
comment: false,
collation: false,

View File

@@ -40,6 +40,7 @@ module.exports = {
unsigned: true,
nullable: true,
zerofill: true,
tableOptions: true,
autoIncrement: true,
comment: true,
collation: true,

View File

@@ -1,11 +1,13 @@
'use strict';
import { app, BrowserWindow, /* session, */ nativeImage } from 'electron';
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
import * as path from 'path';
import Store from 'electron-store';
import * as remoteMain from '@electron/remote/main';
import ipcHandlers from './ipc-handlers';
// remoteMain.initialize();
Store.initRenderer();
const isDevelopment = process.env.NODE_ENV !== 'production';
@@ -30,13 +32,14 @@ async function createMainWindow () {
nodeIntegration: true,
contextIsolation: false,
'web-security': false,
enableRemoteModule: true,
spellcheck: false
},
frame: false,
backgroundColor: '#1d1d1d'
});
remoteMain.enable(window.webContents);
try {
if (isDevelopment) { //
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
@@ -96,7 +99,17 @@ else {
// create main BrowserWindow when electron is ready
app.on('ready', async () => {
mainWindow = await createMainWindow();
Menu.setApplicationMenu(null);
if (isDevelopment)
mainWindow.webContents.openDevTools();
process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error);
});
process.on('unhandledRejection', error => {
mainWindow.webContents.send('unhandled-exception', error);
});
});
}

View File

@@ -30,7 +30,8 @@ export default connections => {
username: conn.sshUser,
password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
passphrase: conn.sshPassphrase
};
}
@@ -85,7 +86,8 @@ export default connections => {
username: conn.sshUser,
password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey
privateKey: conn.sshKey ? fs.readFileSync(conn.sshKey) : null,
passphrase: conn.sshPassphrase
};
}

View File

@@ -112,6 +112,17 @@ export default connections => {
}
});
ipcMain.handle('kill-process', async (event, { uid, pid }) => {
try {
const result = await connections[uid].killProcess(pid);
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

@@ -38,6 +38,26 @@ export default (connections) => {
}
});
ipcMain.handle('get-table-count', async (event, params) => {
try {
const result = await connections[params.uid].getTableApproximateCount(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-table-options', async (event, params) => {
try {
const result = await connections[params.uid].getTableOptions(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-table-indexes', async (event, params) => {
try {
const result = await connections[params.uid].getTableIndexes(params);

View File

@@ -9,7 +9,7 @@ autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true);
export default () => {
ipcMain.on('check-for-updates', event => {
mainWindow = event;
if (process.windowsStore)
if (process.windowsStore || (process.platform === 'linux' && !process.env.APPIMAGE))
mainWindow.reply('no-auto-update');
else {
autoUpdater.checkForUpdatesAndNotify().catch(() => {

View File

@@ -16,6 +16,7 @@ export class AntaresCore {
this._params = args.params;
this._poolSize = args.poolSize || false;
this._connection = null;
this._ssh = null;
this._logger = args.logger || console.log;
this._queryDefaults = {

View File

@@ -2,6 +2,12 @@
import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
const queryLogger = sql => {
// Remove comments, newlines and multiple spaces
const escapedSql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
console.log(escapedSql);
};
export class ClientsFactory {
/**
* Returns a database connection based on received args.
@@ -23,6 +29,8 @@ export class ClientsFactory {
* @memberof ClientsFactory
*/
static getConnection (args) {
args.logger = queryLogger;
switch (args.client) {
case 'mysql':
case 'maria':

View File

@@ -118,13 +118,19 @@ export class MySQLClient extends AntaresCore {
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) {
this._ssh = new SSH2Promise({ ...this._params.ssh });
try {
this._ssh = new SSH2Promise({ ...this._params.ssh });
this._tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = this._tunnel.localPort;
const tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = tunnel.localPort;
}
catch (err) {
if (this._ssh) this._ssh.close();
throw err;
}
}
if (!this._poolSize)
@@ -424,6 +430,56 @@ export class MySQLClient extends AntaresCore {
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table row count
* @memberof MySQLClient
*/
async getTableApproximateCount ({ schema, table }) {
const { rows } = await this.raw(`SELECT table_rows "count" FROM information_schema.tables WHERE table_name = "${table}" AND table_schema = "${schema}"`);
return rows.length ? rows[0].count : 0;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table options
* @memberof MySQLClient
*/
async getTableOptions ({ schema, table }) {
const { rows } = await this.raw(`SHOW TABLE STATUS FROM \`${schema}\` WHERE Name = '${table}'`);
if (rows.length) {
let tableType;
switch (rows[0].Comment) {
case 'VIEW':
tableType = 'view';
break;
default:
tableType = 'table';
break;
}
return {
name: rows[0].Name,
type: tableType,
rows: rows[0].Rows,
created: rows[0].Create_time,
updated: rows[0].Update_time,
engine: rows[0].Engine,
comment: rows[0].Comment,
size: rows[0].Data_length + rows[0].Index_length,
autoIncrement: rows[0].Auto_increment,
collation: rows[0].Collation
};
};
return {};
}
/**
* @param {Object} params
* @param {String} params.schema
@@ -585,7 +641,12 @@ export class MySQLClient extends AntaresCore {
*/
async alterView (params) {
const { view } = params;
let sql = `ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} SQL SECURITY ${view.security} VIEW \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}`;
let sql = `
USE \`${view.schema}\`;
ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''}
SQL SECURITY ${view.security}
VIEW \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}
`;
if (view.name !== view.oldName)
sql += `; RENAME TABLE \`${view.schema}\`.\`${view.oldName}\` TO \`${view.schema}\`.\`${view.name}\``;
@@ -900,10 +961,12 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async createFunction (params) {
const parameters = params.parameters.reduce((acc, curr) => {
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',');
const parameters = 'parameters' in params
? params.parameters.reduce((acc, curr) => {
acc.push(`\`${curr.name}\` ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
return acc;
}, []).join(',')
: '';
const body = params.returns ? params.sql : 'BEGIN\n RETURN 0;\nEND';
@@ -1119,6 +1182,10 @@ export class MySQLClient extends AntaresCore {
});
}
async killProcess (id) {
return await this.raw(`KILL ${id}`);
}
/**
* CREATE TABLE
*
@@ -1126,7 +1193,57 @@ export class MySQLClient extends AntaresCore {
* @memberof MySQLClient
*/
async createTable (params) {
const sql = `CREATE TABLE \`${params.schema}\`.\`${params.name}\` (\`${params.name}_ID\` INT NULL) COMMENT='${params.comment}', COLLATE='${params.collation}', ENGINE=${params.engine}`;
const {
schema,
fields,
foreigns,
indexes,
options
} = params;
const newColumns = [];
const newIndexes = [];
const newForeigns = [];
let sql = `CREATE TABLE \`${schema}\`.\`${options.name}\``;
// ADD FIELDS
fields.forEach(field => {
const typeInfo = this._getTypeInfo(field.type);
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`\`${field.name}\`
${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
${field.autoIncrement ? 'AUTO_INCREMENT' : ''}
${field.default ? `DEFAULT ${field.default}` : ''}
${field.comment ? `COMMENT '${field.comment}'` : ''}
${field.collation ? `COLLATE ${field.collation}` : ''}
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
});
// ADD INDEX
indexes.forEach(index => {
const fields = index.fields.map(field => `\`${field}\``).join(',');
let type = index.type;
if (type === 'PRIMARY')
newIndexes.push(`PRIMARY KEY (${fields})`);
else {
if (type === 'UNIQUE')
type = 'UNIQUE INDEX';
newIndexes.push(`${type} \`${index.name}\` (${fields})`);
}
});
// ADD FOREIGN KEYS
foreigns.forEach(foreign => {
newForeigns.push(`CONSTRAINT \`${foreign.constraintName}\` FOREIGN KEY (\`${foreign.field}\`) REFERENCES \`${foreign.refTable}\` (\`${foreign.refField}\`) ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
});
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')}) COMMENT='${options.comment}', COLLATE='${options.collation}', ENGINE=${options.engine}`;
return await this.raw(sql);
}

View File

@@ -65,13 +65,19 @@ export class PostgreSQLClient extends AntaresCore {
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) {
this._ssh = new SSH2Promise({ ...this._params.ssh });
try {
this._ssh = new SSH2Promise({ ...this._params.ssh });
this._tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = this._tunnel.localPort;
const tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = tunnel.localPort;
}
catch (err) {
if (this._ssh) this._ssh.close();
throw err;
}
}
if (!this._poolSize) {
@@ -94,7 +100,7 @@ export class PostgreSQLClient extends AntaresCore {
}
/**
* Executes an USE query
* Executes an "USE" query
*
* @param {String} schema
* @memberof PostgreSQLClient
@@ -102,7 +108,7 @@ export class PostgreSQLClient extends AntaresCore {
use (schema) {
this._schema = schema;
if (schema)
return this.raw(`SET search_path TO ${schema}`);
return this.raw(`SET search_path TO "${schema}"`);
}
/**
@@ -293,6 +299,53 @@ export class PostgreSQLClient extends AntaresCore {
});
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table row count
* @memberof PostgreSQLClient
*/
async getTableApproximateCount ({ schema, table }) {
const { rows } = await this.raw(`SELECT reltuples AS count FROM pg_class WHERE relname = '${table}'`);
return rows.length ? rows[0].count : 0;
}
/**
* @param {Object} params
* @param {String} params.schema
* @param {String} params.table
* @returns {Object} table options
* @memberof MySQLClient
*/
async getTableOptions ({ schema, table }) {
const { rows } = await this.raw(`
SELECT *,
pg_table_size(QUOTE_IDENT(t.TABLE_SCHEMA) || '.' || QUOTE_IDENT(t.TABLE_NAME))::bigint AS data_length,
pg_relation_size(QUOTE_IDENT(t.TABLE_SCHEMA) || '.' || QUOTE_IDENT(t.TABLE_NAME))::bigint AS index_length,
c.reltuples, obj_description(c.oid) AS comment
FROM "information_schema"."tables" AS t
LEFT JOIN "pg_namespace" n ON t.table_schema = n.nspname
LEFT JOIN "pg_class" c ON n.oid = c.relnamespace AND c.relname=t.table_name
WHERE t."table_schema" = '${schema}'
AND table_name = '${table}'
`);
if (rows.length) {
return {
name: rows[0].table_name,
type: rows[0].table_type === 'VIEW' ? 'view' : 'table',
rows: rows[0].reltuples,
size: +rows[0].data_length + +rows[0].index_length,
collation: rows[0].Collation,
comment: rows[0].comment,
engine: ''
};
};
return {};
}
/**
* @param {Object} params
* @param {String} params.schema
@@ -483,7 +536,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropView (params) {
const sql = `DROP VIEW ${params.schema}.${params.view}`;
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
return await this.raw(sql);
}
@@ -495,10 +548,10 @@ export class PostgreSQLClient extends AntaresCore {
*/
async alterView (params) {
const { view } = params;
let sql = `CREATE OR REPLACE VIEW ${view.schema}.${view.oldName} AS ${view.sql}`;
let sql = `CREATE OR REPLACE VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`;
if (view.name !== view.oldName)
sql += `; ALTER VIEW ${view.schema}.${view.oldName} RENAME TO ${view.name}`;
sql += `; ALTER VIEW "${view.schema}"."${view.oldName}" RENAME TO "${view.name}"`;
return await this.raw(sql);
}
@@ -510,7 +563,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async createView (params) {
const sql = `CREATE VIEW ${params.schema}.${params.name} AS ${params.sql}`;
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
return await this.raw(sql);
}
@@ -711,7 +764,7 @@ export class PostgreSQLClient extends AntaresCore {
async createRoutine (routine) {
const parameters = 'parameters' in routine
? routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} ${curr.name} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
acc.push(`${curr.context} ${curr.name} ${curr.type}`);
return acc;
}, []).join(',')
: '';
@@ -840,7 +893,7 @@ export class PostgreSQLClient extends AntaresCore {
async createFunction (func) {
const parameters = 'parameters' in func
? func.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} ${curr.name || ''} ${curr.type}${curr.length ? `(${curr.length})` : ''}`);
acc.push(`${curr.context} ${curr.name || ''} ${curr.type}`);
return acc;
}, []).join(',')
: '';
@@ -982,6 +1035,10 @@ export class PostgreSQLClient extends AntaresCore {
});
}
async killProcess (id) {
return await this.raw(`SELECT pg_terminate_backend(${id})`);
}
/**
* CREATE TABLE
*
@@ -989,8 +1046,54 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async createTable (params) {
const sql = `CREATE TABLE "${params.schema}"."${params.name}" (${params.name}_id INTEGER NULL); ALTER TABLE "${params.schema}"."${params.name}" DROP COLUMN ${params.name}_id`;
const {
schema,
fields,
foreigns,
indexes,
options
} = params;
const newColumns = [];
const newIndexes = [];
const manageIndexes = [];
const newForeigns = [];
let sql = `CREATE TABLE "${schema}"."${options.name}"`;
// ADD FIELDS
fields.forEach(field => {
const typeInfo = this._getTypeInfo(field.type);
const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false;
newColumns.push(`${field.name}
${field.type.toUpperCase()}${length ? `(${length})` : ''}
${field.unsigned ? 'UNSIGNED' : ''}
${field.zerofill ? 'ZEROFILL' : ''}
${field.nullable ? 'NULL' : 'NOT NULL'}
${field.default ? `DEFAULT ${field.default}` : ''}
${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`);
});
// ADD INDEX
indexes.forEach(index => {
const fields = index.fields.map(field => `${field}`).join(',');
const type = index.type;
if (type === 'PRIMARY')
newIndexes.push(`PRIMARY KEY (${fields})`);
else if (type === 'UNIQUE')
newIndexes.push(`CONSTRAINT "${index.name}" UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX "${index.name}" ON "${schema}"."${options.name}" (${fields})`);
});
// ADD FOREIGN KEYS
foreigns.forEach(foreign => {
newForeigns.push(`CONSTRAINT "${foreign.constraintName}" FOREIGN KEY ("${foreign.field}") REFERENCES "${schema}"."${foreign.refTable}" ("${foreign.refField}") ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`);
});
sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')})`;
if (manageIndexes.length) sql = `${sql}; ${manageIndexes.join(';')}`;
return await this.raw(sql);
}
@@ -1021,45 +1124,36 @@ export class PostgreSQLClient extends AntaresCore {
const createSequences = [];
const manageIndexes = [];
// OPTIONS
if ('comment' in options) alterColumns.push(`COMMENT='${options.comment}'`);
if ('engine' in options) alterColumns.push(`ENGINE=${options.engine}`);
if ('autoIncrement' in options) alterColumns.push(`AUTO_INCREMENT=${+options.autoIncrement}`);
if ('collation' in options) alterColumns.push(`COLLATE='${options.collation}'`);
// ADD FIELDS
additions.forEach(addition => {
const typeInfo = this._getTypeInfo(addition.type);
const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false;
alterColumns.push(`ADD COLUMN ${addition.name}
alterColumns.push(`ADD COLUMN "${addition.name}"
${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''}
${addition.unsigned ? 'UNSIGNED' : ''}
${addition.zerofill ? 'ZEROFILL' : ''}
${addition.nullable ? 'NULL' : 'NOT NULL'}
${addition.autoIncrement ? 'AUTO_INCREMENT' : ''}
${addition.default ? `DEFAULT ${addition.default}` : ''}
${addition.comment ? `COMMENT '${addition.comment}'` : ''}
${addition.collation ? `COLLATE ${addition.collation}` : ''}
${addition.onUpdate ? `ON UPDATE ${addition.onUpdate}` : ''}`);
});
// ADD INDEX
indexChanges.additions.forEach(addition => {
const fields = addition.fields.map(field => `${field}`).join(',');
const fields = addition.fields.map(field => `"${field}"`).join(',');
const type = addition.type;
if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${addition.name} UNIQUE (${fields})`);
alterColumns.push(`ADD CONSTRAINT "${addition.name}" UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX ${addition.name} ON "${schema}"."${table}" (${fields})`);
manageIndexes.push(`CREATE INDEX "${addition.name}" ON "${schema}"."${table}" (${fields})`);
});
// ADD FOREIGN KEYS
foreignChanges.additions.forEach(addition => {
alterColumns.push(`ADD CONSTRAINT ${addition.constraintName} FOREIGN KEY (${addition.field}) REFERENCES ${addition.refTable} (${addition.refField}) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
alterColumns.push(`ADD CONSTRAINT "${addition.constraintName}" FOREIGN KEY ("${addition.field}") REFERENCES "${schema}"."${addition.refTable}" (${addition.refField}) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
});
// CHANGE FIELDS
@@ -1085,6 +1179,7 @@ export class PostgreSQLClient extends AntaresCore {
alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`);
alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`);
if (['SERIAL', 'SMALLSERIAL', 'BIGSERIAL'].includes(change.type)) {
const sequenceName = `${table}_${change.name}_seq`.replace(' ', '_');
createSequences.push(`CREATE SEQUENCE IF NOT EXISTS ${sequenceName} OWNED BY "${table}"."${change.name}"`);
@@ -1102,39 +1197,39 @@ export class PostgreSQLClient extends AntaresCore {
else
manageIndexes.push(`DROP INDEX ${change.oldName}`);
const fields = change.fields.map(field => `${field}`).join(',');
const fields = change.fields.map(field => `"${field}"`).join(',');
const type = change.type;
if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else if (type === 'UNIQUE')
alterColumns.push(`ADD CONSTRAINT ${change.name} UNIQUE (${fields})`);
alterColumns.push(`ADD CONSTRAINT "${change.name}" UNIQUE (${fields})`);
else
manageIndexes.push(`CREATE INDEX ${change.name} ON "${schema}"."${table}" (${fields})`);
manageIndexes.push(`CREATE INDEX "${change.name}" ON "${schema}"."${table}" (${fields})`);
});
// CHANGE FOREIGN KEYS
foreignChanges.changes.forEach(change => {
alterColumns.push(`DROP CONSTRAINT ${change.oldName}`);
alterColumns.push(`ADD CONSTRAINT ${change.constraintName} FOREIGN KEY (${change.field}) REFERENCES ${change.refTable} (${change.refField}) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
alterColumns.push(`DROP CONSTRAINT "${change.oldName}"`);
alterColumns.push(`ADD CONSTRAINT "${change.constraintName}" FOREIGN KEY (${change.field}) REFERENCES "${schema}"."${change.refTable}" ("${change.refField}") ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
});
// DROP FIELDS
deletions.forEach(deletion => {
alterColumns.push(`DROP COLUMN ${deletion.name}`);
alterColumns.push(`DROP COLUMN "${deletion.name}"`);
});
// DROP INDEX
indexChanges.deletions.forEach(deletion => {
if (['PRIMARY', 'UNIQUE'].includes(deletion.type))
alterColumns.push(`DROP CONSTRAINT ${deletion.name}`);
alterColumns.push(`DROP CONSTRAINT "${deletion.name}"`);
else
manageIndexes.push(`DROP INDEX ${deletion.name}`);
manageIndexes.push(`DROP INDEX "${deletion.name}"`);
});
// DROP FOREIGN KEYS
foreignChanges.deletions.forEach(deletion => {
alterColumns.push(`DROP CONSTRAINT ${deletion.constraintName}`);
alterColumns.push(`DROP CONSTRAINT "${deletion.constraintName}"`);
});
if (alterColumns.length) sql += `ALTER TABLE "${schema}"."${table}" ${alterColumns.join(', ')}; `;
@@ -1155,7 +1250,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async duplicateTable (params) {
const sql = `CREATE TABLE ${params.schema}.${params.table}_copy (LIKE ${params.schema}.${params.table} INCLUDING ALL)`;
const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" (LIKE "${params.schema}"."${params.table}" INCLUDING ALL)`;
return await this.raw(sql);
}
@@ -1166,7 +1261,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async truncateTable (params) {
const sql = `TRUNCATE TABLE ${params.schema}.${params.table}`;
const sql = `TRUNCATE TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
@@ -1177,7 +1272,7 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient
*/
async dropTable (params) {
const sql = `DROP TABLE ${params.schema}.${params.table}`;
const sql = `DROP TABLE "${params.schema}"."${params.table}"`;
return await this.raw(sql);
}
@@ -1201,7 +1296,7 @@ export class PostgreSQLClient extends AntaresCore {
else if (Object.keys(this._query.insert).length)
fromRaw = 'INTO';
fromRaw += this._query.from ? ` ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : '';
fromRaw += this._query.from ? ` ${this._query.schema ? `"${this._query.schema}".` : ''}"${this._query.from}" ` : '';
// WHERE
const whereArray = this._query.where.reduce(this._reducer, []);

View File

@@ -29,18 +29,23 @@ export default {
},
computed: {
position () {
const { clientY, clientX } = this.contextEvent;
let topCord = `${clientY + 2}px`;
let leftCord = `${clientX + 5}px`;
let topCord = 0;
let leftCord = 0;
if (this.contextSize) {
if (clientY + (this.contextSize.height < 200 ? 200 : this.contextSize.height) + 5 >= window.innerHeight) {
topCord = `${clientY + 3 - this.contextSize.height}px`;
this.isBottom = true;
if (this.contextEvent) {
const { clientY, clientX } = this.contextEvent;
topCord = `${clientY + 2}px`;
leftCord = `${clientX + 5}px`;
if (this.contextSize) {
if (clientY + (this.contextSize.height < 200 ? 200 : this.contextSize.height) + 5 >= window.innerHeight) {
topCord = `${clientY + 3 - this.contextSize.height}px`;
this.isBottom = true;
}
if (clientX + this.contextSize.width + 5 >= window.innerWidth)
leftCord = `${clientX - this.contextSize.width}px`;
}
if (clientX + this.contextSize.width + 5 >= window.innerWidth)
leftCord = `${clientX - this.contextSize.width}px`;
}
return {
@@ -53,7 +58,8 @@ export default {
window.addEventListener('keydown', this.onKey);
},
mounted () {
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
if (this.$refs.contextContent)
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);

View File

@@ -78,6 +78,7 @@ export default {
.file-uploader-message {
display: flex;
border-radius: $border-radius 0 0 $border-radius;
}
.file-uploader-input {

View File

@@ -31,7 +31,11 @@
class="form-input"
type="text"
>
<span class="input-group-addon field-type" :class="typeClass(parameter.type)">
<span
:title="`${parameter.type} ${parameter.length}`"
class="input-group-addon field-type cut-text"
:class="typeClass(parameter.type)"
>
{{ parameter.type }} {{ parameter.length | wrapNumber }}
</span>
</div>
@@ -127,4 +131,8 @@ export default {
.field-type {
font-size: 0.6rem;
}
.input-group-addon {
max-width: 100px;
}
</style>

View File

@@ -251,7 +251,7 @@ export default {
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = Number.isNaN(+field.default) ? null : +field.default;
fieldDefault = !field.default || Number.isNaN(+field.default.replaceAll('\'', '')) ? null : +field.default.replaceAll('\'', '');
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = field.default
? field.default.includes('\'')

View File

@@ -0,0 +1,282 @@
<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-history mr-1" />
<span class="cut-text">{{ $t('word.history') }}: {{ connectionName }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body p-0 workspace-query-results">
<div
v-if="history.length"
ref="searchForm"
class="form-group has-icon-right p-2 m-0"
>
<input
v-model="searchTerm"
class="form-input"
type="text"
:placeholder="$t('message.searchForQueries')"
>
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" />
<i
v-else
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-4"
@click="searchTerm = ''"
/>
</div>
<div
v-if="history.length"
ref="tableWrapper"
class="vscroll px-1 "
:style="{'height': resultsSize+'px'}"
>
<div ref="table">
<BaseVirtualScroll
ref="resultTable"
:items="filteredHistory"
:item-height="66"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<div
v-for="query in items"
:key="query.uid"
class="tile my-2"
tabindex="0"
>
<div class="tile-icon">
<i class="mdi mdi-code-tags pr-1" />
</div>
<div class="tile-content">
<div class="tile-title">
<code
class="cut-text"
:title="query.sql"
v-html="highlightWord(query.sql)"
/>
</div>
<div class="tile-bottom-content">
<small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small>
<div class="tile-history-buttons">
<button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)">
<i class="mdi mdi-open-in-app pr-1" /> {{ $t('word.select') }}
</button>
<button class="btn btn-link pl-1" @click="copyQuery(query.sql)">
<i class="mdi mdi-content-copy pr-1" /> {{ $t('word.copy') }}
</button>
<button class="btn btn-link pl-1" @click="deleteQuery(query)">
<i class="mdi mdi-delete-forever pr-1" /> {{ $t('word.delete') }}
</button>
</div>
</div>
</div>
</div>
</template>
</BaseVirtualScroll>
</div>
</div>
<div v-else class="empty">
<div class="empty-icon">
<i class="mdi mdi-history mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereIsNoQueriesYet') }}
</p>
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import { mapGetters, mapActions } from 'vuex';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
export default {
name: 'ModalHistory',
components: {
BaseVirtualScroll
},
props: {
connection: Object
},
data () {
return {
resultsSize: 1000,
isQuering: false,
scrollElement: null,
searchTermInterval: null,
searchTerm: '',
localSearchTerm: ''
};
},
computed: {
...mapGetters({
getConnectionName: 'connections/getConnectionName',
getHistoryByWorkspace: 'history/getHistoryByWorkspace'
}),
connectionName () {
return this.getConnectionName(this.connection.uid);
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
},
filteredHistory () {
return this.history.filter(q => q.sql.toLowerCase().search(this.searchTerm.toLowerCase()) >= 0);
}
},
watch: {
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => {
this.localSearchTerm = this.searchTerm;
}, 200);
}
},
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.resizeResults();
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',
deleteQueryFromHistory: 'history/deleteQueryFromHistory'
}),
copyQuery (sql) {
navigator.clipboard.writeText(sql);
},
deleteQuery (query) {
this.deleteQueryFromHistory({
workspace: this.connection.uid,
...query
});
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el)
this.resultsSize = el.offsetHeight - this.$refs.searchForm.offsetHeight;
this.$refs.resultTable.updateWindow();
}
},
formatDate (date) {
return moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date;
},
refreshScroller () {
this.resizeResults();
},
closeModal () {
this.$emit('close');
},
highlightWord (string) {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
}
else
return string;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
};
</script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.tile {
border-radius: $border-radius;
display: flex;
align-items: center;
&:hover,
&:focus {
.tile-content {
.tile-bottom-content {
.tile-history-buttons {
opacity: 1;
}
}
}
}
.tile-icon {
font-size: 1.2rem;
margin-left: 0.3rem;
width: 28px;
}
.tile-content {
padding: 0.3rem;
padding-left: 0.1rem;
max-width: calc(100% - 30px);
code {
max-width: 100%;
display: inline-block;
font-size: 100%;
// color: $primary-color;
opacity: 0.8;
}
.tile-subtitle {
opacity: 0.8;
}
.tile-bottom-content {
display: flex;
justify-content: space-between;
.tile-history-buttons {
opacity: 0;
transition: opacity 0.2s;
button {
font-size: 0.7rem;
height: 1rem;
line-height: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
}
}
}
}
</style>

View File

@@ -1,170 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewFunction"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewFunction') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localFunction.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localFunction.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="localFunction.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="localFunction.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewFunction',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localFunction: {
definer: '',
sql: '',
parameters: [],
name: '',
comment: '',
language: null,
returns: null,
returnsLength: 10,
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
if (this.customizations.languages)
this.localFunction.language = this.customizations.languages[0];
if (this.customizations.functionSql)
this.localFunction.sql = this.customizations.functionSql;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewFunction () {
this.$emit('open-create-function-editor', this.localFunction);
}
}
};
</script>

View File

@@ -1,168 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewRoutine"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewRoutine') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localRoutine.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localRoutine.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localRoutine.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localRoutine.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="localRoutine.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="localRoutine.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewRoutine',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localRoutine: {
definer: '',
sql: '',
parameters: [],
name: '',
comment: '',
language: null,
security: 'DEFINER',
deterministic: false,
dataAccess: 'CONTAINS SQL'
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
if (this.customizations.languages)
this.localRoutine.language = this.customizations.languages[0];
if (this.customizations.procedureSql)
this.localRoutine.sql = this.customizations.procedureSql;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewRoutine () {
this.$emit('open-create-routine-editor', this.localRoutine);
}
}
};
</script>

View File

@@ -1,109 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewTrigger"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewScheduler') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localScheduler.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localScheduler.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localScheduler.comment"
class="form-input"
type="text"
>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewScheduler',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localScheduler: {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
comment: '',
execution: 'EVERY',
every: ['1', 'DAY'],
preserve: true,
state: 'DISABLE'
}
};
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewTrigger () {
this.$emit('open-create-scheduler-editor', this.localScheduler);
}
}
};
</script>

View File

@@ -13,7 +13,7 @@
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<form class="form-horizontal" @submit.prevent="createSchema">
<div class="form-group">
<div class="col-3">
<label class="form-label">{{ $t('word.name') }}</label>

View File

@@ -1,130 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-table-plus mr-1" /> {{ $t('message.createNewTable') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localOptions.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localOptions.comment"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4">
{{ $t('word.collation') }}
</label>
<div class="column">
<select v-model="localOptions.collation" class="form-select">
<option
v-for="collation in workspace.collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</div>
</div>
<div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4">
{{ $t('word.engine') }}
</label>
<div class="column">
<select v-model="localOptions.engine" class="form-select">
<option
v-for="engine in workspace.engines"
:key="engine.name"
:value="engine.name"
>
{{ engine.name }}
</option>
</select>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import { mapGetters } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewTable',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localOptions: {
name: '',
comment: '',
collation: '',
engine: ''
},
isOptionsChanging: false
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
defaultCollation () {
if (this.workspace.customizations.collations)
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
return '';
},
defaultEngine () {
if (this.workspace.customizations.engines)
return this.workspace.engines.find(engine => engine.isDefault).name;
return '';
}
},
mounted () {
this.localOptions.collation = this.defaultCollation;
this.localOptions.engine = this.defaultEngine;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
this.$emit('open-create-table-editor', this.localOptions);
}
}
};
</script>

View File

@@ -1,183 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewTrigger"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewTrigger') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localTrigger.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localTrigger.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.table') }}
</label>
<div class="column">
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.event') }}
</label>
<div class="column">
<div class="input-group">
<select v-model="localTrigger.activation" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select
v-if="!customizations.triggerMultipleEvents"
v-model="localTrigger.event"
class="form-select"
>
<option v-for="event in Object.keys(localEvents)" :key="event">
{{ event }}
</option>
</select>
<div v-if="customizations.triggerMultipleEvents" class="px-4">
<label
v-for="event in Object.keys(localEvents)"
:key="event"
class="form-checkbox form-inline"
@change.prevent="changeEvents(event)"
>
<input :checked="localEvents[event]" type="checkbox"><i class="form-icon" /> {{ event }}
</label>
</div>
</div>
</div>
</div>
</form>
<div v-if="customizations.triggerStatementInCreation" class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor
ref="queryEditor"
:value.sync="localTrigger.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import QueryEditor from '@/components/QueryEditor';
export default {
name: 'ModalNewTrigger',
components: {
ConfirmModal,
QueryEditor
},
props: {
workspace: Object
},
data () {
return {
localTrigger: {
definer: '',
sql: '',
name: '',
table: '',
activation: 'BEFORE',
event: 'INSERT'
},
isOptionsChanging: false,
localEvents: { INSERT: false, UPDATE: false, DELETE: false },
editorHeight: 150
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
},
customizations () {
return this.workspace.customizations;
}
},
created () {
this.localTrigger.table = this.schemaTables.length ? this.schemaTables[0].name : '';
this.localTrigger.sql = this.customizations.triggerSql;
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewTrigger () {
this.$emit('open-create-trigger-editor', this.localTrigger);
},
changeEvents (event) {
if (this.customizations.triggerMultipleEvents) {
this.localEvents[event] = !this.localEvents[event];
this.localTrigger.event = [];
for (const key in this.localEvents) {
if (this.localEvents[key])
this.localTrigger.event.push(key);
}
}
}
}
};
</script>

View File

@@ -1,132 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmNewFunction"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-plus mr-1" /> {{ $t('message.createNewFunction') }}
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="localFunction.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.triggerFunctionlanguages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="localFunction.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'ModalNewTriggerFunction',
components: {
ConfirmModal
},
props: {
workspace: Object
},
data () {
return {
localFunction: {
definer: '',
sql: '',
name: '',
comment: '',
language: null
},
isOptionsChanging: false
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
if (this.customizations.triggerFunctionlanguages)
this.localFunction.language = this.customizations.triggerFunctionlanguages[0];
if (this.customizations.triggerFunctionSql)
this.localFunction.sql = this.customizations.triggerFunctionSql;
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmNewFunction () {
this.$emit('open-create-function-editor', this.localFunction);
}
}
};
</script>

View File

@@ -1,192 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmOptionsChange"
@hide="$emit('close')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-eye-plus mr-1" /> {{ $t('message.createNewView') }}
</div>
</template>
<div :slot="'body'">
<div class="container">
<div class="columns mb-4">
<div class="column col-6">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-6">
<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="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
</div>
</div>
</div>
<div class="columns">
<div class="column col-4">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="DEFINER"
>
<i class="form-icon" /> DEFINER
</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="INVOKER"
>
<i class="form-icon" /> INVOKER
</label>
</div>
</div>
<div class="column col-4">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
name="algorithm"
value="UNDEFINED"
>
<i class="form-icon" /> UNDEFINED
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="MERGE"
name="algorithm"
>
<i class="form-icon" /> MERGE
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="TEMPTABLE"
name="algorithm"
>
<i class="form-icon" /> TEMPTABLE
</label>
</div>
</div>
<div class="column col-4">
<div v-if="workspace.customizations.viewUpdateOption" class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value=""
>
<i class="form-icon" /> None
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="CASCADED"
>
<i class="form-icon" /> CASCADED
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="LOCAL"
>
<i class="form-icon" /> LOCAL
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label>
<QueryEditor
ref="queryEditor"
:value.sync="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import QueryEditor from '@/components/QueryEditor';
export default {
name: 'ModalNewView',
components: {
ConfirmModal,
QueryEditor
},
props: {
workspace: Object
},
data () {
return {
localView: {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
},
isOptionsChanging: false,
editorHeight: 300
};
},
computed: {
schema () {
return this.workspace.breadcrumbs.schema;
}
},
mounted () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
this.$emit('open-create-view-editor', this.localView);
}
}
};
</script>

View File

@@ -1,5 +1,15 @@
<template>
<div class="modal active">
<ModalProcessesListContext
v-if="isContext"
:context-event="contextEvent"
:selected-row="selectedRow"
:selected-cell="selectedCell"
@copy-cell="copyCell"
@copy-row="copyRow"
@kill-process="killProcess"
@close-context="closeContext"
/>
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0 pb-4">
<div class="modal-header pl-2">
@@ -12,34 +22,55 @@
<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 class="workspace-query-buttons">
<div class="dropdown pr-1">
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0 pr-1 d-flex"
:class="{'loading':isQuering}"
title="F5"
@click="getProcessesList"
>
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-1" />
<span>{{ $t('word.refresh') }}</span>
</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="dropdown table-dropdown">
<button
:disabled="isQuering"
class="btn btn-dark btn-sm dropdown-toggle d-flex mr-0 pr-0"
tabindex="0"
>
<i class="mdi mdi-24px mdi-file-export mr-1" />
<span>{{ $t('word.export') }}</span>
<i class="mdi mdi-24px mdi-menu-down" />
</button>
<ul class="menu text-left">
<li class="menu-item">
<a class="c-hand" @click="downloadTable('json')">JSON</a>
</li>
<li class="menu-item">
<a class="c-hand" @click="downloadTable('csv')">CSV</a>
</li>
</ul>
</div>
</div>
<div class="workspace-query-info">
<div v-if="sortedResults.length">
@@ -83,11 +114,12 @@
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<ProcessesListRow
<ModalProcessesListRow
v-for="row in items"
:key="row._id"
:key="row.id"
class="process-row"
:row="row"
@select-row="selectRow(row.id)"
@contextmenu="contextMenu"
@stop-refresh="stopRefresh"
/>
@@ -102,15 +134,18 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import arrayToFile from '../libs/arrayToFile';
import Schema from '@/ipc-api/Schema';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ProcessesListRow from '@/components/ProcessesListRow';
import ModalProcessesListRow from '@/components/ModalProcessesListRow';
import ModalProcessesListContext from '@/components/ModalProcessesListContext';
export default {
name: 'ModalProcessesList',
components: {
BaseVirtualScroll,
ProcessesListRow
ModalProcessesListRow,
ModalProcessesListContext
},
props: {
connection: Object
@@ -119,8 +154,12 @@ export default {
return {
resultsSize: 1000,
isQuering: false,
isContext: false,
autorefreshTimer: 0,
refreshInterval: null,
contextEvent: null,
selectedCell: null,
selectedRow: null,
results: [],
fields: [],
currentSort: '',
@@ -245,10 +284,55 @@ export default {
this.autorefreshTimer = 0;
this.clearRefresh();
},
contextMenu () {},
selectRow (row) {
this.selectedRow = row;
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.stopRefresh();
this.selectedCell = cell;
this.selectedRow = cell.id;
this.contextEvent = event;
this.isContext = true;
},
async killProcess () {
try { // Table data
const { status, response } = await Schema.killProcess({ uid: this.connection.uid, pid: this.selectedRow });
if (status === 'success')
this.getProcessesList();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
closeContext () {
this.isContext = false;
},
copyCell () {
const row = this.results.find(row => row.id === this.selectedRow);
const valueToCopy = row[this.selectedCell.field];
navigator.clipboard.writeText(valueToCopy);
},
copyRow () {
const row = this.results.find(row => row.id === this.selectedRow);
const rowToCopy = JSON.parse(JSON.stringify(row));
navigator.clipboard.writeText(JSON.stringify(rowToCopy));
},
closeModal () {
this.$emit('close');
},
downloadTable (format) {
if (!this.sortedResults) return;
arrayToFile({
type: format,
content: this.sortedResults,
filename: 'processes'
});
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')

View File

@@ -0,0 +1,75 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@close-context="closeContext"
>
<div v-if="selectedRow" class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ $t('word.copy') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div
v-if="selectedRow"
class="context-element"
@click="copyCell"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-numeric-0 mdi-rotate-90 text-light pr-1" /> {{ $tc('word.cell', 1) }}
</span>
</div>
<div
v-if="selectedRow"
class="context-element"
@click="copyRow"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-table-row text-light pr-1" /> {{ $tc('word.row', 1) }}
</span>
</div>
</div>
</div>
<div
v-if="selectedRow"
class="context-element"
@click="killProcess"
>
<span class="d-flex">
<i class="mdi mdi-18px mdi-close-circle-outline text-light pr-1" /> {{ $t('message.killProcess') }}
</span>
</div>
</BaseContextMenu>
</template>
<script>
import BaseContextMenu from '@/components/BaseContextMenu';
export default {
name: 'ModalProcessesListContext',
components: {
BaseContextMenu
},
props: {
contextEvent: MouseEvent,
selectedRow: Number,
selectedCell: Object
},
computed: {
},
methods: {
closeContext () {
this.$emit('close-context');
},
copyCell () {
this.$emit('copy-cell');
this.closeContext();
},
copyRow () {
this.$emit('copy-row');
this.closeContext();
},
killProcess () {
this.$emit('kill-process');
this.closeContext();
}
}
};
</script>

View File

@@ -1,21 +1,18 @@
<template>
<div class="tr" @click="selectRow($event, row._id)">
<div class="tr" @click="selectRow()">
<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 })"
@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>
<span
v-if="!isInlineEditor[cKey]"
class="cell-content"
:class="`${isNull(col)} type-${typeof col === 'number' ? 'int' : 'varchar'}`"
@dblclick="dblClick(cKey)"
>{{ col | cutText }}</span>
</div>
<ConfirmModal
v-if="isInfoModal"
@@ -51,7 +48,7 @@ import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
export default {
name: 'ProcessesListRow',
name: 'ModalProcessesListRow',
components: {
ConfirmModal,
TextEditor
@@ -73,25 +70,15 @@ export default {
};
},
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);
selectRow () {
this.$emit('select-row');
},
openContext (event, payload) {
if (this.isEditable) {
payload.field = this.fields[payload.field].name;// Ensures field name only
this.$emit('contextmenu', event, payload);
}
this.$emit('contextmenu', event, payload);
},
hideInfoModal () {
this.isInfoModal = false;
@@ -126,6 +113,7 @@ export default {
.cell-content {
display: block;
padding: 0 0.2rem;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -59,15 +59,15 @@
<div class="column col-12 h6 text-uppercase mb-1">
{{ $t('word.application') }}
</div>
<div class="column col-8 col-sm-12 mb-2">
<div class="column col-9 col-sm-12 mb-2">
<div class="form-group">
<div class="col-6 col-sm-12">
<div class="col-7 col-sm-12">
<label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<select
v-model="localLocale"
class="form-select"
@@ -84,12 +84,12 @@
</div>
</div>
<div class="form-group">
<div class="col-6 col-sm-12">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.dataTabPageSize') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<select
v-model="localPageSize"
class="form-select"
@@ -104,13 +104,13 @@
</select>
</div>
</div>
<div class="form-group">
<div class="col-6 col-sm-12">
<div class="form-group mb-0">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.restorePreviourSession') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleRestoreSession">
<input type="checkbox" :checked="restoreTabs">
<i class="form-icon" />
@@ -118,12 +118,12 @@
</div>
</div>
<div class="form-group">
<div class="col-6 col-sm-12">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.notificationsTimeout') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<div class="input-group">
<input
v-model="localTimeout"
@@ -141,29 +141,27 @@
<div class="column col-12 h6 mt-4 text-uppercase mb-1">
{{ $t('word.editor') }}
</div>
<div class="column col-8 col-sm-12">
<div class="form-group">
<div class="col-6 col-sm-12">
<div class="column col-9 col-sm-12">
<div class="form-group mb-0">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('word.autoCompletion') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleAutoComplete">
<input type="checkbox" :checked="selectedAutoComplete">
<i class="form-icon" />
</label>
</div>
</div>
</div>
<div class="column col-8 col-sm-12">
<div class="form-group">
<div class="col-6 col-sm-12">
<div class="form-group mb-0">
<div class="col-7 col-sm-12">
<label class="form-label">
{{ $t('message.wrapLongLines') }}
</label>
</div>
<div class="col-6 col-sm-12">
<div class="col-5 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleLineWrap">
<input type="checkbox" :checked="selectedLineWrap">
<i class="form-icon" />
@@ -320,7 +318,7 @@ export default {
localTimeout: null,
localEditorTheme: null,
selectedTab: 'general',
pageSizes: [40, 100, 250, 500, 1000],
pageSizes: [30, 40, 100, 250, 500, 1000],
editorThemes: [
{
group: this.$t('word.light'),

View File

@@ -117,7 +117,7 @@ export default {
},
getStatusBadge (uid) {
if (this.getWorkspace(uid)) {
const status = this.getWorkspace(uid).connection_status;
const status = this.getWorkspace(uid).connectionStatus;
switch (status) {
case 'connected':

View File

@@ -1,11 +1,11 @@
<template>
<div v-show="isSelected" class="workspace column columns col-gapless">
<WorkspaceExploreBar
v-if="workspace.connection_status === 'connected'"
v-if="workspace.connectionStatus === 'connected'"
:connection="connection"
:is-selected="isSelected"
/>
<div v-if="workspace.connection_status === 'connected'" class="workspace-tabs column columns col-gapless">
<div v-if="workspace.connectionStatus === 'connected'" class="workspace-tabs column columns col-gapless">
<Draggable
ref="tabWrap"
v-model="draggableTabs"
@@ -18,19 +18,21 @@
<li
v-for="(tab, i) of draggableTabs"
:key="i"
:ref="selectedTab === tab.uid ? 'tab-selected' : ''"
class="tab-item tab-draggable"
draggable="true"
:class="{'active': selectedTab === tab.uid}"
@mousedown="selectTab({uid: workspace.uid, tab: tab.uid})"
@mousedown.left="selectTab({uid: workspace.uid, tab: tab.uid})"
@mouseup.middle="closeTab(tab)"
>
<a v-if="tab.type === 'query'" class="tab-link">
<i class="mdi mdi-18px mdi-code-tags mr-1" />
<span>
Query #{{ tab.index }}
<span>{{ tab.content || 'Query' | cutText }} #{{ tab.index }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
@@ -42,11 +44,12 @@
@dblclick="openAsPermanentTab(tab)"
>
<i class="mdi mdi-18px mr-1" :class="tab.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span :title="`${$t('word.data').toUpperCase()}: ${tab.elementType}`">
<span class=" text-italic">{{ tab.elementName }}</span>
<span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
<span class=" text-italic">{{ tab.elementName | cutText }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
@@ -54,11 +57,29 @@
<a v-else-if="tab.type === 'data'" class="tab-link">
<i class="mdi mdi-18px mr-1" :class="tab.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<span :title="`${$t('word.data').toUpperCase()}: ${tab.elementType}`">
{{ tab.elementName }}
<span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ tab.elementName | cutText }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-table'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newTable') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
@@ -70,11 +91,12 @@
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
{{ tab.elementName }}
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ tab.elementName | cutText }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
@@ -86,11 +108,114 @@
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
{{ tab.elementName }}
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.view`)}`">
{{ tab.elementName | cutText }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-view'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newView') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-trigger'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newTrigger') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-routine'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newRoutine') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-function'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newFunction') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-trigger-function'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newTriggerFunction') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'new-scheduler'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-shape-square-plus mdi-18px mr-1" />
<span :title="`${$t('word.new').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ $t('message.newScheduler') }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
@@ -103,11 +228,12 @@
@dblclick="openAsPermanentTab(tab)"
>
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
<span class=" text-italic">{{ tab.elementName }}</span>
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
<span class=" text-italic">{{ tab.elementName | cutText }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
@@ -119,48 +245,16 @@
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
{{ tab.elementName }}
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
{{ tab.elementName | cutText }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@mousedown.left.stop
@click.stop="closeTab(tab)"
/>
</span>
</a>
<!-- <a
v-else-if="tab.type === 'temp-trigger-function-props'"
class="tab-link"
:class="{'badge': tab.isChanged}"
@dblclick="openAsPermanentTab(tab)"
>
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
<span class=" text-italic">{{ tab.elementName }}</span>
<span
class="btn btn-clear"
:title="$t('word.close')"
@click.stop="closeTab(tab)"
/>
</span>
</a>
<a
v-else-if="tab.type === 'trigger-function-props'"
class="tab-link"
:class="{'badge': tab.isChanged}"
>
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
{{ tab.elementName }}
<span
class="btn btn-clear"
:title="$t('word.close')"
@click.stop="closeTab(tab)"
/>
</span>
</a> -->
</li>
<li slot="header" class="tab-item dropdown tools-dropdown">
<a
@@ -209,22 +303,16 @@
</a>
</li>
</Draggable>
<!--<WorkspacePropsTabScheduler
v-show="selectedTab === 'prop' && workspace.breadcrumbs.scheduler"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:scheduler="workspace.breadcrumbs.scheduler"
/> -->
<WorkspaceEmptyState v-if="!workspace.tabs.length" @new-tab="addQueryTab" />
<template v-for="tab of workspace.tabs">
<WorkspaceQueryTab
<WorkspaceTabQuery
v-if="tab.type==='query'"
:key="tab.uid"
:tab="tab"
:is-selected="selectedTab === tab.uid"
:connection="connection"
/>
<WorkspaceTableTab
<WorkspaceTabTable
v-else-if="['temp-data', 'data'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
@@ -233,7 +321,15 @@
:schema="tab.schema"
:element-type="tab.elementType"
/>
<WorkspacePropsTab
<WorkspaceTabNewTable
v-else-if="tab.type === 'new-table'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:schema="tab.schema"
/>
<WorkspaceTabPropsTable
v-else-if="tab.type === 'table-props'"
:key="tab.uid"
:connection="connection"
@@ -241,7 +337,15 @@
:table="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabView
<WorkspaceTabNewView
v-else-if="tab.type === 'new-view'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:schema="tab.schema"
/>
<WorkspaceTabPropsView
v-else-if="tab.type === 'view-props'"
:key="tab.uid"
:is-selected="selectedTab === tab.uid"
@@ -249,7 +353,16 @@
:view="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabTrigger
<WorkspaceTabNewTrigger
v-else-if="tab.type === 'new-trigger'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsTrigger
v-else-if="['temp-trigger-props', 'trigger-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
@@ -257,7 +370,16 @@
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabTriggerFunction
<WorkspaceTabNewTriggerFunction
v-else-if="tab.type === 'new-trigger-function'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsTriggerFunction
v-else-if="['temp-trigger-function-props', 'trigger-function-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
@@ -265,7 +387,16 @@
:function="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabRoutine
<WorkspaceTabNewRoutine
v-else-if="tab.type === 'new-routine'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsRoutine
v-else-if="['temp-routine-props', 'routine-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
@@ -273,7 +404,16 @@
:routine="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabFunction
<WorkspaceTabNewFunction
v-else-if="tab.type === 'new-function'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsFunction
v-else-if="['temp-function-props', 'function-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
@@ -281,7 +421,16 @@
:function="tab.elementName"
:schema="tab.schema"
/>
<WorkspacePropsTabScheduler
<WorkspaceTabNewScheduler
v-else-if="tab.type === 'new-scheduler'"
:key="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid"
:trigger="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsScheduler
v-else-if="['temp-scheduler-props', 'scheduler-props'].includes(tab.type)"
:key="tab.uid"
:connection="connection"
@@ -313,15 +462,24 @@ import Connection from '@/ipc-api/Connection';
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel';
import WorkspaceQueryTab from '@/components/WorkspaceQueryTab';
import WorkspaceTableTab from '@/components/WorkspaceTableTab';
import WorkspacePropsTab from '@/components/WorkspacePropsTab';
import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabTriggerFunction from '@/components/WorkspacePropsTabTriggerFunction';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
import WorkspacePropsTabFunction from '@/components/WorkspacePropsTabFunction';
import WorkspacePropsTabScheduler from '@/components/WorkspacePropsTabScheduler';
import WorkspaceTabQuery from '@/components/WorkspaceTabQuery';
import WorkspaceTabTable from '@/components/WorkspaceTabTable';
import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable';
import WorkspaceTabNewView from '@/components/WorkspaceTabNewView';
import WorkspaceTabNewTrigger from '@/components/WorkspaceTabNewTrigger';
import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine';
import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction';
import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler';
import WorkspaceTabNewTriggerFunction from '@/components/WorkspaceTabNewTriggerFunction';
import WorkspaceTabPropsTable from '@/components/WorkspaceTabPropsTable';
import WorkspaceTabPropsView from '@/components/WorkspaceTabPropsView';
import WorkspaceTabPropsTrigger from '@/components/WorkspaceTabPropsTrigger';
import WorkspaceTabPropsTriggerFunction from '@/components/WorkspaceTabPropsTriggerFunction';
import WorkspaceTabPropsRoutine from '@/components/WorkspaceTabPropsRoutine';
import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction';
import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler';
import ModalProcessesList from '@/components/ModalProcessesList';
import ModalDiscardChanges from '@/components/ModalDiscardChanges';
@@ -332,18 +490,34 @@ export default {
WorkspaceEmptyState,
WorkspaceExploreBar,
WorkspaceEditConnectionPanel,
WorkspaceQueryTab,
WorkspaceTableTab,
WorkspacePropsTab,
WorkspacePropsTabView,
WorkspacePropsTabTrigger,
WorkspacePropsTabTriggerFunction,
WorkspacePropsTabRoutine,
WorkspacePropsTabFunction,
WorkspacePropsTabScheduler,
WorkspaceTabQuery,
WorkspaceTabTable,
WorkspaceTabNewTable,
WorkspaceTabPropsTable,
WorkspaceTabNewView,
WorkspaceTabPropsView,
WorkspaceTabNewTrigger,
WorkspaceTabPropsTrigger,
WorkspaceTabNewTriggerFunction,
WorkspaceTabPropsTriggerFunction,
WorkspaceTabNewRoutine,
WorkspaceTabNewFunction,
WorkspaceTabPropsRoutine,
WorkspaceTabPropsFunction,
WorkspaceTabNewScheduler,
WorkspaceTabPropsScheduler,
ModalProcessesList,
ModalDiscardChanges
},
filters: {
cutText (string) {
const limit = 20;
const escapedString = string.replace(/\s{2,}/g, ' ');
if (escapedString.length > limit)
return `${escapedString.substr(0, limit)}...`;
return escapedString;
}
},
props: {
connection: Object
},
@@ -384,7 +558,7 @@ export default {
return false;
},
selectedTab () {
return this.workspace.selected_tab;
return this.workspace ? this.workspace.selectedTab : null;
},
queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query');
@@ -397,6 +571,20 @@ export default {
return false;
}
},
watch: {
selectedTab (newVal, oldVal) {
if (newVal !== oldVal) {
setTimeout(() => {
const element = this.$refs['tab-selected'] ? this.$refs['tab-selected'][0] : null;
if (element) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
}, 50);
}
}
},
async created () {
await this.addWorkspace(this.connection.uid);
const isInitiated = await Connection.checkConnection(this.connection.uid);
@@ -597,7 +785,7 @@ export default {
.th {
position: sticky;
top: 0;
border: 1px solid;
border: 2px solid;
border-left: none;
border-bottom-width: 2px;
padding: 0;
@@ -606,15 +794,15 @@ export default {
z-index: 1;
> div {
padding: 0.1rem 0.4rem;
padding: 0.1rem 0.2rem;
min-width: -webkit-fill-available;
}
}
.td {
border-right: 1px solid;
border-bottom: 1px solid;
padding: 0 0.4rem;
border-right: 2px solid;
border-bottom: 2px solid;
padding: 0 0.2rem;
text-overflow: ellipsis;
max-width: 200px;
white-space: nowrap;

View File

@@ -309,6 +309,18 @@
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.passphrase') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="connection.sshPassphrase"
class="form-input"
type="password"
>
</div>
</div>
</fieldset>
</form>
</div>
@@ -393,6 +405,13 @@ export default {
return this.isConnecting || this.isTesting;
}
},
watch: {
'connection.client' () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
}
},
created () {
this.setDefaults();
@@ -432,7 +451,7 @@ export default {
try {
const res = await Connection.makeTest(this.connection);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
@@ -455,7 +474,7 @@ export default {
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}

View File

@@ -303,6 +303,18 @@
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-4 col-sm-12">
<label class="form-label">{{ $t('word.passphrase') }}</label>
</div>
<div class="column col-8 col-sm-12">
<input
v-model="localConnection.sshPassphrase"
class="form-input"
type="password"
>
</div>
</div>
</fieldset>
</form>
</div>
@@ -414,7 +426,7 @@ export default {
try {
const res = await Connection.makeTest(this.localConnection);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
@@ -437,7 +449,7 @@ export default {
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message });
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
@@ -454,6 +466,7 @@ export default {
closeAsking () {
this.isTesting = false;
this.isAsking = false;
this.isConnecting = false;
},
selectTab (tab) {
this.selectedTab = tab;

View File

@@ -5,10 +5,13 @@
ref="explorebar"
class="workspace-explorebar column"
:style="{width: localWidth ? localWidth+'px' : ''}"
tabindex="0"
@keypress="explorebarSearch"
@keydown="explorebarSearch"
>
<div class="workspace-explorebar-header">
<span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connection_status === 'connected'" class="workspace-explorebar-tools">
<span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools">
<i
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewSchema')"
@@ -28,17 +31,23 @@
</span>
</div>
<div class="workspace-explorebar-search">
<div v-if="workspace.connection_status === 'connected'" class="has-icon-right">
<div v-if="workspace.connectionStatus === 'connected'" class="has-icon-right">
<input
ref="searchInput"
v-model="searchTerm"
class="form-input input-sm"
type="text"
:placeholder="$t('message.searchForElements')"
>
<i class="form-icon mdi mdi-magnify mdi-18px" />
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px" />
<i
v-else
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-1"
@click="searchTerm = ''"
/>
</div>
</div>
<div class="workspace-explorebar-body">
<div class="workspace-explorebar-body" @click="$refs.explorebar.focus()">
<WorkspaceExploreBarSchema
v-for="db of workspace.structure"
:key="db.name"
@@ -56,60 +65,18 @@
@close="hideNewDBModal"
@reload="refresh"
/>
<ModalNewTable
v-if="isNewTableModal"
:workspace="workspace"
@close="hideCreateTableModal"
@open-create-table-editor="openCreateTableEditor"
/>
<ModalNewView
v-if="isNewViewModal"
:workspace="workspace"
@close="hideCreateViewModal"
@open-create-view-editor="openCreateViewEditor"
/>
<ModalNewTrigger
v-if="isNewTriggerModal"
:workspace="workspace"
@close="hideCreateTriggerModal"
@open-create-trigger-editor="openCreateTriggerEditor"
/>
<ModalNewRoutine
v-if="isNewRoutineModal"
:workspace="workspace"
@close="hideCreateRoutineModal"
@open-create-routine-editor="openCreateRoutineEditor"
/>
<ModalNewFunction
v-if="isNewFunctionModal"
:workspace="workspace"
@close="hideCreateFunctionModal"
@open-create-function-editor="openCreateFunctionEditor"
/>
<ModalNewTriggerFunction
v-if="isNewTriggerFunctionModal"
:workspace="workspace"
@close="hideCreateTriggerFunctionModal"
@open-create-function-editor="openCreateTriggerFunctionEditor"
/>
<ModalNewScheduler
v-if="isNewSchedulerModal"
:workspace="workspace"
@close="hideCreateSchedulerModal"
@open-create-scheduler-editor="openCreateSchedulerEditor"
/>
<DatabaseContext
v-if="isDatabaseContext"
:selected-schema="selectedSchema"
:context-event="databaseContextEvent"
@close-context="closeDatabaseContext"
@show-create-table-modal="showCreateTableModal"
@show-create-view-modal="showCreateViewModal"
@show-create-trigger-modal="showCreateTriggerModal"
@show-create-routine-modal="showCreateRoutineModal"
@show-create-function-modal="showCreateFunctionModal"
@show-create-trigger-function-modal="showCreateTriggerFunctionModal"
@show-create-scheduler-modal="showCreateSchedulerModal"
@open-create-table-tab="openCreateElementTab('table')"
@open-create-view-tab="openCreateElementTab('view')"
@open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-routine-tab="openCreateElementTab('routine')"
@open-create-function-tab="openCreateElementTab('function')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-scheduler-tab="openCreateElementTab('scheduler')"
@reload="refresh"
/>
<TableContext
@@ -117,6 +84,8 @@
:selected-schema="selectedSchema"
:selected-table="selectedTable"
:context-event="tableContextEvent"
@delete-table="deleteTable"
@duplicate-table="duplicateTable"
@close-context="closeTableContext"
@reload="refresh"
/>
@@ -133,11 +102,11 @@
:selected-misc="selectedMisc"
:selected-schema="selectedSchema"
:context-event="miscContextEvent"
@show-create-trigger-modal="showCreateTriggerModal"
@show-create-routine-modal="showCreateRoutineModal"
@show-create-function-modal="showCreateFunctionModal"
@show-create-trigger-function-modal="showCreateTriggerFunctionModal"
@show-create-scheduler-modal="showCreateSchedulerModal"
@open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-routine-tab="openCreateElementTab('routine')"
@open-create-function-tab="openCreateElementTab('function')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-scheduler-tab="openCreateElementTab('scheduler')"
@close-context="closeMiscFolderContext"
@reload="refresh"
/>
@@ -149,8 +118,6 @@ import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
@@ -160,13 +127,6 @@ import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext';
import ModalNewSchema from '@/components/ModalNewSchema';
import ModalNewTable from '@/components/ModalNewTable';
import ModalNewView from '@/components/ModalNewView';
import ModalNewTrigger from '@/components/ModalNewTrigger';
import ModalNewRoutine from '@/components/ModalNewRoutine';
import ModalNewFunction from '@/components/ModalNewFunction';
import ModalNewTriggerFunction from '@/components/ModalNewTriggerFunction';
import ModalNewScheduler from '@/components/ModalNewScheduler';
export default {
name: 'WorkspaceExploreBar',
@@ -176,14 +136,7 @@ export default {
TableContext,
MiscContext,
MiscFolderContext,
ModalNewSchema,
ModalNewTable,
ModalNewView,
ModalNewTrigger,
ModalNewRoutine,
ModalNewFunction,
ModalNewTriggerFunction,
ModalNewScheduler
ModalNewSchema
},
props: {
connection: Object,
@@ -194,7 +147,6 @@ export default {
isRefreshing: false,
isNewDBModal: false,
isNewTableModal: false,
isNewViewModal: false,
isNewTriggerModal: false,
isNewRoutineModal: false,
@@ -275,9 +227,12 @@ export default {
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
selectTab: 'workspaces/selectTab',
newTab: 'workspaces/newTab',
removeTabs: 'workspaces/removeTabs',
setSearchTerm: 'workspaces/setSearchTerm',
addNotification: 'notifications/addNotification',
changeExplorebarSize: 'settings/changeExplorebarSize'
changeExplorebarSize: 'settings/changeExplorebarSize',
addLoadingElement: 'workspaces/addLoadingElement',
removeLoadingElement: 'workspaces/removeLoadingElement'
}),
async refresh () {
if (!this.isRefreshing) {
@@ -286,6 +241,19 @@ export default {
this.isRefreshing = false;
}
},
explorebarSearch (e) {
if (e.code === 'Backspace') {
e.preventDefault();
if (this.searchTerm.length)
this.searchTerm = this.searchTerm.slice(0, -1);
else
return;
}
else if (e.key.length > 1)// Prevent non-alphanumerics
return;
this.$refs.searchInput.focus();
},
resize (e) {
const el = this.$refs.explorebar;
let explorebarWidth = e.pageX - el.getBoundingClientRect().left;
@@ -302,34 +270,17 @@ export default {
hideNewDBModal () {
this.isNewDBModal = false;
},
showCreateTableModal () {
openCreateElementTab (element) {
this.closeDatabaseContext();
this.isNewTableModal = true;
},
hideCreateTableModal () {
this.isNewTableModal = false;
},
async openCreateTableEditor (payload) {
const params = {
uid: this.connection.uid,
this.closeMiscFolderContext();
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Tables.createTable(params);
if (status === 'success') {
await this.refresh();
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'table',
type: 'table-props'
});
}
else
this.addNotification({ status: 'error', message: response });
elementName: '',
elementType: element,
type: `new-${element}`
});
},
openSchemaContext (payload) {
this.selectedSchema = payload.schema;
@@ -366,38 +317,6 @@ export default {
closeMiscFolderContext () {
this.isMiscFolderContext = false;
},
showCreateViewModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewViewModal = true;
},
hideCreateViewModal () {
this.isNewViewModal = false;
},
async openCreateViewEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Views.createView(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, view: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'view',
type: 'view-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateTriggerModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
@@ -406,31 +325,6 @@ export default {
hideCreateTriggerModal () {
this.isNewTriggerModal = false;
},
async openCreateTriggerEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Triggers.createTrigger(params);
if (status === 'success') {
await this.refresh();
const triggerName = this.customizations.triggerTableInName ? `${payload.table}.${payload.name}` : payload.name;
this.changeBreadcrumbs({ schema: this.selectedSchema, trigger: triggerName });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: triggerName,
elementType: 'trigger',
type: 'trigger-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateRoutineModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
@@ -439,30 +333,6 @@ export default {
hideCreateRoutineModal () {
this.isNewRoutineModal = false;
},
async openCreateRoutineEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Routines.createRoutine(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, routine: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'routine',
type: 'routine-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
showCreateFunctionModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
@@ -487,6 +357,89 @@ export default {
hideCreateSchedulerModal () {
this.isNewSchedulerModal = false;
},
async deleteTable (payload) {
this.closeTableContext();
this.addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
let res;
if (payload.table.type === 'table') {
res = await Tables.dropTable({
uid: this.connection.uid,
table: payload.table.name,
schema: payload.schema
});
}
else if (payload.table.type === 'view') {
res = await Views.dropView({
uid: this.connection.uid,
view: payload.table.name,
schema: payload.schema
});
}
const { status, response } = res;
if (status === 'success') {
this.refresh();
this.removeTabs({
uid: this.connection.uid,
elementName: payload.table.name,
elementType: payload.table.type,
schema: payload.schema
});
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
},
async duplicateTable (payload) {
this.closeTableContext();
this.addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
const { status, response } = await Tables.duplicateTable({
uid: this.connection.uid,
table: payload.table.name,
schema: payload.schema
});
if (status === 'success')
this.refresh();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
},
async openCreateFunctionEditor (payload) {
const params = {
uid: this.connection.uid,
@@ -591,6 +544,10 @@ export default {
position: relative;
padding: 0;
&:focus {
outline: none;
}
.workspace-explorebar-header {
width: 100%;
padding: 0.3rem;

View File

@@ -101,9 +101,6 @@ export default {
removeTabs: 'workspaces/removeTabs',
newTab: 'workspaces/newTab'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},

View File

@@ -6,35 +6,35 @@
<div
v-if="selectedMisc === 'trigger'"
class="context-element"
@click="$emit('show-create-trigger-modal')"
@click="$emit('open-create-trigger-tab')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $t('message.createNewTrigger') }}</span>
</div>
<div
v-if="selectedMisc === 'procedure'"
class="context-element"
@click="$emit('show-create-routine-modal')"
@click="$emit('open-create-routine-tab')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle text-light pr-1" /> {{ $t('message.createNewRoutine') }}</span>
</div>
<div
v-if="selectedMisc === 'function'"
class="context-element"
@click="$emit('show-create-function-modal')"
@click="$emit('open-create-function-tab')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box text-light pr-1" /> {{ $t('message.createNewFunction') }}</span>
</div>
<div
v-if="selectedMisc === 'triggerFunction'"
class="context-element"
@click="$emit('show-create-trigger-function-modal')"
@click="$emit('open-create-trigger-function-tab')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-cog-clockwise text-light pr-1" /> {{ $t('message.createNewFunction') }}</span>
</div>
<div
v-if="selectedMisc === 'scheduler'"
class="context-element"
@click="$emit('show-create-scheduler-modal')"
@click="$emit('open-create-scheduler-tab')"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $t('message.createNewScheduler') }}</span>
</div>
@@ -74,9 +74,6 @@ export default {
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},

View File

@@ -16,15 +16,21 @@
<ul class="menu menu-nav pt-0">
<li
v-for="table of filteredTables"
:ref="breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name) ? 'explorebar-selected' : ''"
:key="table.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@mousedown="selectTable({schema: database.name, table})"
:class="{'selected': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@mousedown.left="selectTable({schema: database.name, table})"
@dblclick="openDataTab({schema: database.name, table})"
@contextmenu.prevent="showTableContext($event, table)"
>
<a class="table-name">
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
<div v-if="checkLoadingStatus(table.name, 'table')" class="icon loading mr-1" />
<i
v-else
class="table-icon mdi mdi-18px mr-1"
:class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'"
/>
<span v-html="highlightWord(table.name)" />
</a>
<div
@@ -54,9 +60,10 @@
<li
v-for="trigger of filteredTriggers"
:key="trigger.name"
:ref="breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@mousedown="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@mousedown.left="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: trigger, type: 'trigger'})"
@contextmenu.prevent="showMiscContext($event, {...trigger, type: 'trigger'})"
>
@@ -87,9 +94,10 @@
<li
v-for="(procedure, i) of filteredProcedures"
:key="`${procedure.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name}"
@mousedown="selectMisc({schema: database.name, misc: procedure, type: 'routine'})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name}"
@mousedown.left="selectMisc({schema: database.name, misc: procedure, type: 'routine'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: procedure, type: 'routine'})"
@contextmenu.prevent="showMiscContext($event, {...procedure, type: 'procedure'})"
>
@@ -120,9 +128,10 @@
<li
v-for="(func, i) of filteredTriggerFunctions"
:key="`${func.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}"
@mousedown="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: func, type: 'triggerFunction'})"
@contextmenu.prevent="showMiscContext($event, {...func, type: 'triggerFunction'})"
>
@@ -153,9 +162,10 @@
<li
v-for="(func, i) of filteredFunctions"
:key="`${func.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.function === func.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@mousedown="selectMisc({schema: database.name, misc: func, type: 'function'})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'function'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: func, type: 'function'})"
@contextmenu.prevent="showMiscContext($event, {...func, type: 'function'})"
>
@@ -186,9 +196,10 @@
<li
v-for="scheduler of filteredSchedulers"
:key="scheduler.name"
:ref="breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name ? 'explorebar-selected' : ''"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@mousedown="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@mousedown.left="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: scheduler, type: 'scheduler'})"
@contextmenu.prevent="showMiscContext($event, {...scheduler, type: 'scheduler'})"
>
@@ -251,11 +262,14 @@ export default {
filteredSchedulers () {
return this.database.schedulers.filter(scheduler => scheduler.name.search(this.searchTerm) >= 0);
},
workspace () {
return this.getWorkspace(this.connection.uid);
},
breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs;
return this.workspace.breadcrumbs;
},
customizations () {
return this.getWorkspace(this.connection.uid).customizations;
return this.workspace.customizations;
},
loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid);
@@ -270,6 +284,27 @@ export default {
return this.database.tables.reduce((acc, curr) => acc + curr.size, 0);
}
},
watch: {
breadcrumbs (newVal, oldVal) {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
setTimeout(() => {
const element = this.$refs['explorebar-selected'] ? this.$refs['explorebar-selected'][0] : null;
if (element) {
const rect = element.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
const isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
if (!isVisible) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
}
}, 50);
}
}
},
methods: {
...mapActions({
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
@@ -285,11 +320,16 @@ export default {
this.addLoadedSchema(schema);
this.isLoading = false;
}
this.changeBreadcrumbs({ schema, table: null });
},
selectTable ({ schema, table }) {
this.newTab({ uid: this.connection.uid, elementName: table.name, schema: this.database.name, type: 'temp-data', elementType: table.type });
this.newTab({
uid: this.connection.uid,
elementName: table.name,
schema: this.database.name,
type: 'temp-data',
elementType: table.type
});
this.setBreadcrumbs({ schema, [table.type]: table.name });
},
selectMisc ({ schema, misc, type }) {
@@ -365,6 +405,12 @@ export default {
}
else
return string;
},
checkLoadingStatus (name, type) {
return this.workspace.loadingElements.some(el =>
el.name === name &&
el.type === type &&
el.schema === this.database.name);
}
}
};
@@ -372,6 +418,10 @@ export default {
<style lang="scss">
.workspace-explorebar-database {
.selected {
font-weight: 700;
}
.database-name {
position: sticky;
top: 0;
@@ -428,7 +478,8 @@ export default {
line-height: 1.2;
position: relative;
&:hover {
&:hover,
&.selected {
border-radius: $border-radius;
}
}

View File

@@ -10,49 +10,49 @@
<div
v-if="workspace.customizations.tableAdd"
class="context-element"
@click="showCreateTableModal"
@click="openCreateTableTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div>
<div
v-if="workspace.customizations.viewAdd"
class="context-element"
@click="showCreateViewModal"
@click="openCreateViewTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
</div>
<div
v-if="workspace.customizations.triggerAdd"
class="context-element"
@click="showCreateTriggerModal"
@click="openCreateTriggerTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div>
<div
v-if="workspace.customizations.routineAdd"
class="context-element"
@click="showCreateRoutineModal"
@click="openCreateRoutineTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div>
<div
v-if="workspace.customizations.functionAdd"
class="context-element"
@click="showCreateFunctionModal"
@click="openCreateFunctionTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div>
<div
v-if="workspace.customizations.triggerFunctionAdd"
class="context-element"
@click="showCreateTriggerFunctionModal"
@click="openCreateTriggerFunctionTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-cog-clockwise pr-1" /> {{ $tc('word.triggerFunction', 1) }}</span>
</div>
<div
v-if="workspace.customizations.schedulerAdd"
class="context-element"
@click="showCreateSchedulerModal"
@click="openCreateSchedulerTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div>
@@ -132,26 +132,26 @@ export default {
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
openCreateTableTab () {
this.$emit('open-create-table-tab');
},
showCreateViewModal () {
this.$emit('show-create-view-modal');
openCreateViewTab () {
this.$emit('open-create-view-tab');
},
showCreateTriggerModal () {
this.$emit('show-create-trigger-modal');
openCreateTriggerTab () {
this.$emit('open-create-trigger-tab');
},
showCreateRoutineModal () {
this.$emit('show-create-routine-modal');
openCreateRoutineTab () {
this.$emit('open-create-routine-tab');
},
showCreateFunctionModal () {
this.$emit('show-create-function-modal');
openCreateFunctionTab () {
this.$emit('open-create-function-tab');
},
showCreateTriggerFunctionModal () {
this.$emit('show-create-trigger-function-modal');
openCreateTriggerFunctionTab () {
this.$emit('open-create-trigger-function-tab');
},
showCreateSchedulerModal () {
this.$emit('show-create-scheduler-modal');
openCreateSchedulerTab () {
this.$emit('open-create-scheduler-tab');
},
showDeleteModal () {
this.isDeleteModal = true;

View File

@@ -4,28 +4,28 @@
@close-context="closeContext"
>
<div
v-if="selectedTable.type === 'table' && workspace.customizations.tableSettings"
v-if="selectedTable && selectedTable.type === 'table' && customizations.tableSettings"
class="context-element"
@click="openTableSettingTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-tune-vertical-variant text-light pr-1" /> {{ $t('word.settings') }}</span>
</div>
<div
v-if="selectedTable.type === 'view' && workspace.customizations.viewSettings"
v-if="selectedTable && selectedTable.type === 'view' && customizations.viewSettings"
class="context-element"
@click="openViewSettingTab"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-tune-vertical-variant text-light pr-1" /> {{ $t('word.settings') }}</span>
</div>
<div
v-if="selectedTable.type === 'table'"
v-if="selectedTable && selectedTable.type === 'table'"
class="context-element"
@click="duplicateTable"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-multiple text-light pr-1" /> {{ $t('message.duplicateTable') }}</span>
</div>
<div
v-if="selectedTable.type === 'table'"
v-if="selectedTable && selectedTable.type === 'table'"
class="context-element"
@click="showEmptyModal"
>
@@ -76,7 +76,6 @@ import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal';
import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
export default {
name: 'WorkspaceExploreBarTableContext',
@@ -102,6 +101,9 @@ export default {
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.workspace && this.workspace.customizations ? this.workspace.customizations : {};
}
},
methods: {
@@ -109,11 +111,10 @@ export default {
addNotification: 'notifications/addNotification',
newTab: 'workspaces/newTab',
removeTabs: 'workspaces/removeTabs',
addLoadingElement: 'workspaces/addLoadingElement',
removeLoadingElement: 'workspaces/removeLoadingElement',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
showCreateTableModal () {
this.$emit('show-create-table-modal');
},
showDeleteModal () {
this.isDeleteModal = true;
},
@@ -161,26 +162,18 @@ export default {
this.closeContext();
},
async duplicateTable () {
try {
const { status, response } = await Tables.duplicateTable({
uid: this.selectedWorkspace,
table: this.selectedTable.name,
schema: this.selectedSchema
});
if (status === 'success') {
this.closeContext();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
duplicateTable () {
this.$emit('duplicate-table', { schema: this.selectedSchema, table: this.selectedTable });
},
async emptyTable () {
this.closeContext();
this.addLoadingElement({
name: this.selectedTable.name,
schema: this.selectedSchema,
type: 'table'
});
try {
const { status, response } = await Tables.truncateTable({
uid: this.selectedWorkspace,
@@ -188,55 +181,23 @@ export default {
schema: this.selectedSchema
});
if (status === 'success') {
this.closeContext();
if (status === 'success')
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: this.selectedTable.name,
schema: this.selectedSchema,
type: 'table'
});
},
async deleteTable () {
try {
let res;
if (this.selectedTable.type === 'table') {
res = await Tables.dropTable({
uid: this.selectedWorkspace,
table: this.selectedTable.name,
schema: this.selectedSchema
});
}
else if (this.selectedTable.type === 'view') {
res = await Views.dropView({
uid: this.selectedWorkspace,
view: this.selectedTable.name,
schema: this.selectedSchema
});
}
const { status, response } = res;
if (status === 'success') {
this.removeTabs({
uid: this.selectedWorkspace,
elementName: this.selectedTable.name,
elementType: this.selectedTable.type,
schema: this.selectedSchema
});
this.closeContext();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
deleteTable () {
this.$emit('delete-table', { schema: this.selectedSchema, table: this.selectedTable });
}
}
};

View File

@@ -1,213 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span class="cut-text">{{ $t('word.options') }} "{{ localOptions.name }}"</span>
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="optionsProxy.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="optionsProxy.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.returns') }}
</label>
<div class="column">
<div class="input-group">
<select
v-model="optionsProxy.returns"
class="form-select text-uppercase"
style="width: 0;"
>
<option v-if="localOptions.returns === 'VOID'">
VOID
</option>
<option v-if="!isInDataTypes">
{{ localOptions.returns }}
</option>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="optionsProxy.returns === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
<input
v-if="customizations.parametersLength"
v-model="optionsProxy.returnsLength"
class="form-input"
type="number"
min="0"
>
</div>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="optionsProxy.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="optionsProxy.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.functionDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="optionsProxy.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.functionDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="optionsProxy.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsFunctionOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
},
isInDataTypes () {
let typeNames = [];
for (const group of this.workspace.dataTypes) {
typeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
}
return typeNames.includes(this.localOptions.returns);
}
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
if (!this.isTableNameValid)
this.optionsProxy.name = this.localOptions.name;
this.$emit('options-update', this.optionsProxy);
}
}
};
</script>

View File

@@ -1,132 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span class="cut-text">{{ $t('word.options') }} "{{ table }}"</span>
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="optionsProxy.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="optionsProxy.comment"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.autoIncrement" class="form-group">
<label class="form-label col-4">
{{ $t('word.autoIncrement') }}
</label>
<div class="column">
<input
v-model="optionsProxy.autoIncrement"
class="form-input"
type="number"
:disabled="optionsProxy.autoIncrement === null"
>
</div>
</div>
<div v-if="workspace.customizations.collations" class="form-group">
<label class="form-label col-4">
{{ $t('word.collation') }}
</label>
<div class="column">
<select v-model="optionsProxy.collation" class="form-select">
<option
v-for="collation in workspace.collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</div>
</div>
<div v-if="workspace.customizations.engines" class="form-group">
<label class="form-label col-4">
{{ $t('word.engine') }}
</label>
<div class="column">
<select v-model="optionsProxy.engine" class="form-select">
<option
v-for="engine in workspace.engines"
:key="engine.name"
:value="engine.name"
>
{{ engine.name }}
</option>
</select>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
table: String,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
}
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
if (!this.isTableNameValid)
this.optionsProxy.name = this.localOptions.name;
this.$emit('options-update', this.optionsProxy);
}
}
};
</script>

View File

@@ -1,161 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span class="cut-text">{{ $t('word.options') }} "{{ localOptions.name }}"</span>
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="optionsProxy.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="optionsProxy.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="optionsProxy.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="optionsProxy.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDataAccess" class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="optionsProxy.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDeterministic" class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="optionsProxy.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
}
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
if (!this.isTableNameValid)
this.optionsProxy.name = this.localOptions.name;
this.$emit('options-update', this.optionsProxy);
}
}
};
</script>

View File

@@ -1,125 +0,0 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span class="cut-text">{{ $t('word.options') }} "{{ localOptions.name }}"</span>
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div v-if="customizations.triggerFunctionlanguages" class="form-group">
<label class="form-label col-4">
{{ $t('word.language') }}
</label>
<div class="column">
<select v-model="optionsProxy.language" class="form-select">
<option v-for="language in customizations.triggerFunctionlanguages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="optionsProxy.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="optionsProxy.comment"
class="form-input"
type="text"
>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsTriggerFunctionOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
},
customizations () {
return this.workspace.customizations;
},
isInDataTypes () {
let typeNames = [];
for (const group of this.workspace.dataTypes) {
typeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
}
return typeNames.includes(this.localOptions.returns);
}
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
if (!this.isTableNameValid)
this.optionsProxy.name = this.localOptions.name;
this.$emit('options-update', this.optionsProxy);
}
}
};
</script>

View File

@@ -0,0 +1,397 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
</label>
<input
ref="firstInput"
v-model="localFunction.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
</label>
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
</label>
<select
v-if="workspace.users.length"
v-model="localFunction.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.returns') }}
</label>
<div class="input-group">
<select
v-model="localFunction.returns"
class="form-select text-uppercase"
style="max-width: 150px;"
>
<option v-if="localFunction.returns === 'VOID'">
VOID
</option>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="localFunction.returns === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
<input
v-if="customizations.parametersLength"
v-model="localFunction.returnsLength"
style="max-width: 150px;"
class="form-input"
type="number"
min="0"
:placeholder="$t('word.length')"
>
</div>
</div>
</div>
<div v-if="customizations.comment" class="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
</label>
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
</label>
<select v-model="localFunction.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.functionDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
</label>
<select v-model="localFunction.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.functionDeterministic" class="column col-auto">
<div class="form-group">
<label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localFunction.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspaceTabPropsFunctionParamsModal
v-if="isParamsModal"
:local-parameters="localFunction.parameters"
:workspace="workspace"
:func="localFunction.name"
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspaceTabNewFunction',
components: {
BaseLoader,
QueryEditor,
WorkspaceTabPropsFunctionParamsModal
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
originalFunction: {},
localFunction: {},
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalFunction = {
sql: this.customizations.functionSql,
language: this.customizations.languages ? this.customizations.languages[0] : null,
name: '',
definer: '',
comment: '',
security: 'DEFINER',
dataAccess: 'CONTAINS SQL',
deterministic: false,
returns: this.workspace.dataTypes.length ? this.workspace.dataTypes[0].types[0].name : null
};
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localFunction
};
try {
const { status, response } = await Functions.createFunction(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localFunction.name,
elementType: 'function',
type: 'function-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, function: this.localFunction.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
optionsUpdate (options) {
this.localFunction = options;
},
parametersUpdate (parameters) {
this.localFunction = { ...this.localFunction, parameters };
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

View File

@@ -0,0 +1,353 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
</label>
<input
ref="firstInput"
v-model="localRoutine.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
</label>
<select v-model="localRoutine.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
</label>
<select
v-if="workspace.users.length"
v-model="localRoutine.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
</label>
<input
v-model="localRoutine.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
</label>
<select v-model="localRoutine.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
</label>
<select v-model="localRoutine.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDeterministic" class="column col-auto">
<div class="form-group">
<label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline">
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor
v-show="isSelected"
:key="`new-${_uid}`"
ref="queryEditor"
:value.sync="localRoutine.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspaceTabPropsRoutineParamsModal
v-if="isParamsModal"
:local-parameters="localRoutine.parameters"
:workspace="workspace"
routine="new"
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal';
import Routines from '@/ipc-api/Routines';
export default {
name: 'WorkspaceTabNewRoutine',
components: {
QueryEditor,
BaseLoader,
WorkspaceTabPropsRoutineParamsModal
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
originalRoutine: {},
localRoutine: {},
lastRoutine: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
isChanged () {
return JSON.stringify(this.originalRoutine) !== JSON.stringify(this.localRoutine);
},
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalRoutine = {
sql: this.customizations.procedureSql,
language: this.customizations.languages ? this.customizations.languages[0] : null,
name: '',
definer: '',
comment: '',
security: 'DEFINER',
dataAccess: 'CONTAINS SQL',
deterministic: false
};
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localRoutine
};
try {
const { status, response } = await Routines.createRoutine(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localRoutine.name,
elementType: 'routine',
type: 'routine-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, routine: this.localRoutine.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

View File

@@ -0,0 +1,323 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showTimingModal">
<i class="mdi mdi-24px mdi-timer mr-1" />
<span>{{ $t('word.timing') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localScheduler.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localScheduler.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalScheduler.definer">
{{ originalScheduler.definer.replaceAll('`', '') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="column">
<div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label>
<input
v-model="localScheduler.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="column">
<div class="form-group">
<label class="form-label mr-2">{{ $t('word.state') }}</label>
<label class="form-radio form-inline">
<input
v-model="localScheduler.state"
type="radio"
name="state"
value="ENABLE"
><i class="form-icon" /> ENABLE
</label>
<label class="form-radio form-inline">
<input
v-model="localScheduler.state"
type="radio"
name="state"
value="DISABLE"
><i class="form-icon" /> DISABLE
</label>
<label class="form-radio form-inline">
<input
v-model="localScheduler.state"
type="radio"
name="state"
value="DISABLE ON SLAVE"
><i class="form-icon" /> DISABLE ON SLAVE
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localScheduler.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspaceTabPropsSchedulerTimingModal
v-if="isTimingModal"
:local-options="localScheduler"
:workspace="workspace"
@hide="hideTimingModal"
@options-update="timingUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal';
import Schedulers from '@/ipc-api/Schedulers';
export default {
name: 'WorkspaceTabNewScheduler',
components: {
BaseLoader,
QueryEditor,
WorkspaceTabPropsSchedulerTimingModal
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isTimingModal: false,
originalScheduler: {},
localScheduler: {},
lastScheduler: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
tabUid () {
return this.$vnode.key;
},
isChanged () {
return JSON.stringify(this.originalScheduler) !== JSON.stringify(this.localScheduler);
},
isDefinerInUsers () {
return this.originalScheduler ? this.workspace.users.some(user => this.originalScheduler.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
this.originalScheduler = {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
comment: '',
execution: 'EVERY',
every: ['1', 'DAY'],
preserve: true,
state: 'DISABLE'
};
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localScheduler
};
try {
const { status, response } = await Schedulers.createScheduler(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localScheduler.name,
elementType: 'scheduler',
type: 'scheduler-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, scheduler: this.localScheduler.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
showTimingModal () {
this.isTimingModal = true;
},
hideTimingModal () {
this.isTimingModal = false;
},
timingUpdate (options) {
this.localScheduler = options;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

View File

@@ -0,0 +1,452 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
</button>
<button
:disabled="!isChanged || isSaving"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button
:disabled="isSaving"
class="btn btn-dark btn-sm"
:title="$t('message.addNewField')"
@click="addField"
>
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $t('word.add') }}</span>
</button>
<button
:disabled="isSaving || !localFields.length"
class="btn btn-dark btn-sm"
:title="$t('message.manageIndexes')"
@click="showIntdexesModal"
>
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" />
<span>{{ $t('word.indexes') }}</span>
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isSaving || !localFields.length"
@click="showForeignModal"
>
<i class="mdi mdi-24px mdi-key-link mr-1" />
<span>{{ $t('word.foreignKeys') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localOptions.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.comment" class="column">
<div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label>
<input
v-model="localOptions.comment"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.collations" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.collation') }}
</label>
<select v-model="localOptions.collation" class="form-select">
<option
v-for="collation in workspace.collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</div>
</div>
<div v-if="workspace.customizations.engines" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.engine') }}
</label>
<select v-model="localOptions.engine" class="form-select">
<option
v-for="engine in workspace.engines"
:key="engine.name"
:value="engine.name"
>
{{ engine.name }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 p-relative">
<BaseLoader v-if="isLoading" />
<WorkspaceTabNewTableEmptyState v-if="!localFields.length" @new-field="addField" />
<WorkspaceTabPropsTableFields
v-if="localFields.length"
ref="indexTable"
:fields="localFields"
:indexes="localIndexes"
:foreigns="localKeyUsage"
:tab-uid="tabUid"
:conn-uid="connection.uid"
:index-types="workspace.indexTypes"
table="new"
:schema="schema"
mode="table"
@duplicate-field="duplicateField"
@remove-field="removeField"
@add-new-index="addNewIndex"
@add-to-index="addToIndex"
@rename-field="renameField"
/>
</div>
<WorkspaceTabPropsTableIndexesModal
v-if="isIndexesModal"
:local-indexes="localIndexes"
table="new"
:fields="localFields"
:index-types="workspace.indexTypes"
:workspace="workspace"
@hide="hideIndexesModal"
@indexes-update="indexesUpdate"
/>
<WorkspaceTabPropsTableForeignModal
v-if="isForeignModal"
:local-key-usage="localKeyUsage"
:connection="connection"
table="new"
:schema="schema"
:schema-tables="schemaTables"
:fields="localFields"
:workspace="workspace"
@hide="hideForeignModal"
@foreigns-update="foreignsUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabPropsTableFields from '@/components/WorkspaceTabPropsTableFields';
import WorkspaceTabPropsTableIndexesModal from '@/components/WorkspaceTabPropsTableIndexesModal';
import WorkspaceTabPropsTableForeignModal from '@/components/WorkspaceTabPropsTableForeignModal';
import WorkspaceTabNewTableEmptyState from '@/components/WorkspaceTabNewTableEmptyState';
export default {
name: 'WorkspaceTabNewTable',
components: {
BaseLoader,
WorkspaceTabPropsTableFields,
WorkspaceTabPropsTableIndexesModal,
WorkspaceTabPropsTableForeignModal,
WorkspaceTabNewTableEmptyState
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isIndexesModal: false,
isForeignModal: false,
isOptionsChanging: false,
originalFields: [],
localFields: [],
originalKeyUsage: [],
localKeyUsage: [],
originalIndexes: [],
localIndexes: [],
tableOptions: {},
localOptions: {},
lastTable: null,
newFieldsCounter: 0
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected',
getDatabaseVariable: 'workspaces/getDatabaseVariable'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
tabUid () {
return this.$vnode.key;
},
defaultCollation () {
if (this.workspace.customizations.collations)
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
return '';
},
defaultEngine () {
if (this.workspace.customizations.engines)
return this.workspace.engines.find(engine => engine.isDefault).name;
return '';
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
},
isChanged () {
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) ||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) ||
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions);
}
},
watch: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.tableOptions = {
name: '',
type: 'table',
engine: this.defaultEngine,
comment: '',
collation: this.defaultCollation
};
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
renameTabs: 'workspaces/renameTabs',
removeTab: 'workspaces/removeTab',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
fields: this.localFields,
foreigns: this.localKeyUsage,
indexes: this.localIndexes,
options: this.localOptions
};
try {
const { status, response } = await Tables.createTable(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localOptions.name,
elementType: 'table',
type: 'table-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, table: this.localOptions.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
this.newFieldsCounter = 0;
},
clearChanges () {
this.localFields = JSON.parse(JSON.stringify(this.originalFields));
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes));
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
this.tableOptions = {
name: '',
type: 'table',
engine: this.defaultEngine,
comment: '',
collation: this.defaultCollation
};
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
this.newFieldsCounter = 0;
},
addField () {
this.localFields.push({
_id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '',
type: this.workspace.dataTypes[0].types[0].name,
schema: this.schema,
numPrecision: null,
numLength: this.workspace.dataTypes[0].types[0].length,
datePrecision: null,
charLength: null,
nullable: false,
unsigned: false,
zerofill: false,
order: this.localFields.length + 1,
default: null,
charset: null,
collation: null,
autoIncrement: false,
onUpdate: '',
comment: ''
});
setTimeout(() => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20);
},
renameField (payload) {
this.localIndexes = this.localIndexes.map(index => {
const fi = index.fields.findIndex(field => field === payload.old);
if (fi !== -1)
index.fields[fi] = payload.new;
return index;
});
this.localKeyUsage = this.localKeyUsage.map(key => {
if (key.field === payload.old)
key.field = payload.new;
return key;
});
},
duplicateField (uid) {
const fieldToClone = Object.assign({}, this.localFields.find(field => field._id === uid));
fieldToClone._id = uidGen();
fieldToClone.name = `${fieldToClone.name}_copy`;
fieldToClone.order = this.localFields.length + 1;
this.localFields = [...this.localFields, fieldToClone];
setTimeout(() => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20);
},
removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid);
},
addNewIndex (payload) {
this.localIndexes = [...this.localIndexes, {
_id: uidGen(),
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
fields: [payload.field],
type: payload.index,
comment: '',
indexType: 'BTREE',
indexComment: '',
cardinality: 0
}];
},
addToIndex (payload) {
this.localIndexes = this.localIndexes.map(index => {
if (index._id === payload.index) index.fields.push(payload.field);
return index;
});
},
optionsUpdate (options) {
this.localOptions = options;
},
showIntdexesModal () {
this.isIndexesModal = true;
},
hideIndexesModal () {
this.isIndexesModal = false;
},
indexesUpdate (indexes) {
this.localIndexes = indexes;
},
showForeignModal () {
this.isForeignModal = true;
},
hideForeignModal () {
this.isForeignModal = false;
},
foreignsUpdate (foreigns) {
this.localKeyUsage = foreigns;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

View File

@@ -0,0 +1,33 @@
<template>
<div class="column col-12 empty">
<p class="h6 empty-subtitle">
{{ $t('message.thereAreNoTableFields') }}
</p>
<div class="empty-action">
<button class="btn btn-gray d-flex" @click="$emit('new-field')">
<i class="mdi mdi-24px mdi-playlist-plus mr-2" />
{{ $t('message.addNewField') }}
</button>
</div>
</div>
</template>
<script>
export default {
name: 'WorkspaceTabNewTableEmptyState'
};
</script>
<style scoped>
.empty {
border-radius: 0;
background: transparent;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-left: auto;
margin-right: auto;
z-index: 9;
}
</style>

View File

@@ -0,0 +1,323 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localTrigger.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localTrigger.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalTrigger.definer">
{{ originalTrigger.definer.replaceAll('`', '') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<fieldset class="column columns mb-0" :disabled="customizations.triggerOnlyRename">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label>
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label>
<div class="input-group">
<select v-model="localTrigger.activation" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select
v-if="!customizations.triggerMultipleEvents"
v-model="localTrigger.event"
class="form-select"
>
<option v-for="event in Object.keys(localEvents)" :key="event">
{{ event }}
</option>
</select>
<div v-if="customizations.triggerMultipleEvents" class="px-4">
<label
v-for="event in Object.keys(localEvents)"
:key="event"
class="form-checkbox form-inline"
@change.prevent="changeEvents(event)"
>
<input :checked="localEvents[event]" type="checkbox"><i class="form-icon" /> {{ event }}
</label>
</div>
</div>
</div>
</div>
</fieldset>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localTrigger.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script>
import QueryEditor from '@/components/QueryEditor';
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import Triggers from '@/ipc-api/Triggers';
export default {
name: 'WorkspaceTabNewTrigger',
components: {
BaseLoader,
QueryEditor
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
originalTrigger: {},
localTrigger: {},
lastTrigger: null,
sqlProxy: '',
editorHeight: 300,
localEvents: { INSERT: false, UPDATE: false, DELETE: false }
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
tabUid () {
return this.$vnode.key;
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalTrigger) !== JSON.stringify(this.localTrigger);
},
isDefinerInUsers () {
return this.originalTrigger ? this.workspace.users.some(user => this.originalTrigger.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalTrigger = {
sql: this.customizations.triggerSql,
definer: '',
table: this.schemaTables.length ? this.schemaTables[0].name : null,
activation: 'BEFORE',
event: this.customizations.triggerMultipleEvents ? ['INSERT'] : 'INSERT',
name: ''
};
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
changeEvents (event) {
if (this.customizations.triggerMultipleEvents) {
this.localEvents[event] = !this.localEvents[event];
this.localTrigger.event = [];
for (const key in this.localEvents) {
if (this.localEvents[key])
this.localTrigger.event.push(key);
}
}
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localTrigger
};
try {
const { status, response } = await Triggers.createTrigger(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localTrigger.name,
elementType: 'trigger',
type: 'trigger-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, trigger: this.localTrigger.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
Object.keys(this.localEvents).forEach(event => {
this.localEvents[event] = false;
});
if (this.customizations.triggerMultipleEvents) {
this.originalTrigger.event.forEach(e => {
this.localEvents[e] = true;
});
}
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

View File

@@ -0,0 +1,279 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
</label>
<input
ref="firstInput"
v-model="localFunction.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="customizations.triggerFunctionlanguages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
</label>
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.triggerFunctionlanguages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
</label>
<select
v-if="workspace.users.length"
v-model="localFunction.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
</label>
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localFunction.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspaceTabNewTriggerFunction',
components: {
BaseLoader,
QueryEditor
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: {},
localFunction: {},
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalFunction = {
sql: this.customizations.triggerFunctionSql,
language: this.customizations.triggerFunctionlanguages.length ? this.customizations.triggerFunctionlanguages[0] : null,
name: ''
};
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localFunction
};
try {
const { status, response } = await Functions.createTriggerFunction(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localFunction.name,
elementType: 'triggerFunction',
type: 'trigger-function-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, triggerFunction: this.localFunction.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

View File

@@ -0,0 +1,287 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
title="CTRL+S"
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
ref="firstInput"
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<select
v-if="workspace.users.length"
v-model="localView.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option v-if="!isDefinerInUsers" :value="originalView.definer">
{{ originalView.definer.replaceAll('`', '') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<select v-model="localView.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<select v-model="localView.algorithm" class="form-select">
<option>UNDEFINED</option>
<option>MERGE</option>
<option>TEMPTABLE</option>
</select>
</div>
</div>
<div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<select v-model="localView.updateOption" class="form-select">
<option value="">
None
</option>
<option>CASCADED</option>
<option>LOCAL</option>
</select>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
:value.sync="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import Views from '@/ipc-api/Views';
export default {
name: 'WorkspaceTabNewView',
components: {
BaseLoader,
QueryEditor
},
props: {
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
data () {
return {
isLoading: false,
isSaving: false,
originalView: {},
localView: {},
lastView: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
tabUid () {
return this.$vnode.key;
},
isChanged () {
return JSON.stringify(this.originalView) !== JSON.stringify(this.localView);
},
isDefinerInUsers () {
return this.originalView ? this.workspace.users.some(user => this.originalView.definer === `\`${user.name}\`@\`${user.host}\``) : true;
}
},
watch: {
isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, view: this.view });
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
}
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
this.originalView = {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
};
this.localView = JSON.parse(JSON.stringify(this.originalView));
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeDestroy () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
newTab: 'workspaces/newTab',
removeTab: 'workspaces/removeTab',
renameTabs: 'workspaces/renameTabs'
}),
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localView
};
try {
const { status, response } = await Views.createView(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localView.name,
elementType: 'view',
type: 'view-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, view: this.localView.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localView = JSON.parse(JSON.stringify(this.originalView));
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
}
}
};
</script>

View File

@@ -37,10 +37,6 @@
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
@@ -49,6 +45,152 @@
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
</label>
<input
ref="firstInput"
v-model="localFunction.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
</label>
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
</label>
<select
v-if="workspace.users.length"
v-model="localFunction.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.returns') }}
</label>
<div class="input-group">
<select
v-model="localFunction.returns"
class="form-select text-uppercase"
style="max-width: 150px;"
>
<option v-if="localFunction.returns === 'VOID'">
VOID
</option>
<option v-if="!isInDataTypes">
{{ localFunction.returns }}
</option>
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="localFunction.returns === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
<input
v-if="customizations.parametersLength"
v-model="localFunction.returnsLength"
style="max-width: 150px;"
class="form-input"
type="number"
min="0"
:placeholder="$t('word.length')"
>
</div>
</div>
</div>
<div v-if="customizations.comment" class="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
</label>
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
</label>
<select v-model="localFunction.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.functionDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
</label>
<select v-model="localFunction.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.functionDeterministic" class="column col-auto">
<div class="form-group">
<label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label>
@@ -61,14 +203,7 @@
:height="editorHeight"
/>
</div>
<WorkspacePropsFunctionOptionsModal
v-if="isOptionsModal"
:local-options="localFunction"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsFunctionParamsModal
<WorkspaceTabPropsFunctionParamsModal
v-if="isParamsModal"
:local-parameters="localFunction.parameters"
:workspace="workspace"
@@ -91,18 +226,16 @@ 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 WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspacePropsTabFunction',
name: 'WorkspaceTabPropsFunction',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsFunctionOptionsModal,
WorkspacePropsFunctionParamsModal,
WorkspaceTabPropsFunctionParamsModal,
ModalAskParameters
},
props: {
@@ -115,7 +248,6 @@ export default {
return {
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
@@ -133,6 +265,9 @@ export default {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
@@ -144,6 +279,19 @@ export default {
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
isTableNameValid () {
return this.localFunction.name !== '';
},
isInDataTypes () {
let typeNames = [];
for (const group of this.workspace.dataTypes) {
typeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
}
return typeNames.includes(this.localFunction.returns);
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
@@ -330,12 +478,6 @@ export default {
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},

View File

@@ -176,7 +176,7 @@ import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineParamsModal',
name: 'WorkspaceTabPropsFunctionParamsModal',
components: {
ConfirmModal
},
@@ -242,7 +242,7 @@ export default {
name: `Param${this.i++}`,
type: 'INT',
context: 'IN',
length: 10
length: ''
}];
if (this.parametersProxy.length === 1)

View File

@@ -37,10 +37,6 @@
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
@@ -49,6 +45,108 @@
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
</label>
<input
ref="firstInput"
v-model="localRoutine.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
type="text"
>
</div>
</div>
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
</label>
<select v-model="localRoutine.language" class="form-select">
<option v-for="language in customizations.languages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
</label>
<select
v-if="workspace.users.length"
v-model="localRoutine.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
</label>
<input
v-model="localRoutine.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
</label>
<select v-model="localRoutine.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
</label>
<select v-model="localRoutine.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div v-if="customizations.procedureDeterministic" class="column col-auto">
<div class="form-group">
<label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline">
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
@@ -62,14 +160,7 @@
:height="editorHeight"
/>
</div>
<WorkspacePropsRoutineOptionsModal
v-if="isOptionsModal"
:local-options="localRoutine"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsRoutineParamsModal
<WorkspaceTabPropsRoutineParamsModal
v-if="isParamsModal"
:local-parameters="localRoutine.parameters"
:workspace="workspace"
@@ -92,18 +183,16 @@ 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 WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Routines from '@/ipc-api/Routines';
export default {
name: 'WorkspacePropsTabRoutine',
name: 'WorkspaceTabPropsRoutine',
components: {
QueryEditor,
BaseLoader,
WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal,
WorkspaceTabPropsRoutineParamsModal,
ModalAskParameters
},
props: {
@@ -116,7 +205,6 @@ export default {
return {
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalRoutine: null,
@@ -134,6 +222,9 @@ export default {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
@@ -143,6 +234,9 @@ export default {
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
isTableNameValid () {
return this.localRoutine.name !== '';
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
@@ -327,12 +421,6 @@ export default {
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},

View File

@@ -176,7 +176,7 @@ import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineParamsModal',
name: 'WorkspaceTabPropsRoutineParamsModal',
components: {
ConfirmModal
},

View File

@@ -37,7 +37,7 @@
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
@@ -77,7 +77,7 @@
</select>
</div>
</div>
<div class="column col-4">
<div class="column">
<div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label>
<input
@@ -87,8 +87,6 @@
>
</div>
</div>
</div>
<div class="columns">
<div class="column">
<div class="form-group">
<label class="form-label mr-2">{{ $t('word.state') }}</label>
@@ -132,7 +130,7 @@
:height="editorHeight"
/>
</div>
<WorkspacePropsSchedulerTimingModal
<WorkspaceTabPropsSchedulerTimingModal
v-if="isTimingModal"
:local-options="localScheduler"
:workspace="workspace"
@@ -146,15 +144,15 @@
import { mapGetters, mapActions } from 'vuex';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsSchedulerTimingModal from '@/components/WorkspacePropsSchedulerTimingModal';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal';
import Schedulers from '@/ipc-api/Schedulers';
export default {
name: 'WorkspacePropsTabScheduler',
name: 'WorkspaceTabPropsScheduler',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsSchedulerTimingModal
WorkspaceTabPropsSchedulerTimingModal
},
props: {
connection: Object,

View File

@@ -143,7 +143,7 @@ import { VueMaskDirective } from 'v-mask';
import moment from 'moment';
export default {
name: 'WorkspacePropsSchedulerTimingModal',
name: 'WorkspaceTabPropsSchedulerTimingModal',
components: {
ConfirmModal
},

View File

@@ -51,14 +51,6 @@
<i class="mdi mdi-24px mdi-key-link mr-1" />
<span>{{ $t('word.foreignKeys') }}</span>
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isSaving"
@click="showOptionsModal"
>
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')">
@@ -67,9 +59,80 @@
</div>
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<input
v-model="localOptions.name"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.comment" class="column">
<div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label>
<input
v-model="localOptions.comment"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="workspace.customizations.autoIncrement" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.autoIncrement') }}
</label>
<input
ref="firstInput"
v-model="localOptions.autoIncrement"
class="form-input"
type="number"
:disabled="localOptions.autoIncrement === null"
>
</div>
</div>
<div v-if="workspace.customizations.collations" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.collation') }}
</label>
<select v-model="localOptions.collation" class="form-select">
<option
v-for="collation in workspace.collations"
:key="collation.id"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</div>
</div>
<div v-if="workspace.customizations.engines" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.engine') }}
</label>
<select v-model="localOptions.engine" class="form-select">
<option
v-for="engine in workspace.engines"
:key="engine.name"
:value="engine.name"
>
{{ engine.name }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 p-relative">
<BaseLoader v-if="isLoading" />
<WorkspacePropsTable
<WorkspaceTabPropsTableFields
v-if="localFields"
ref="indexTable"
:fields="localFields"
@@ -81,21 +144,14 @@
:table="table"
:schema="schema"
mode="table"
@duplicate-field="duplicateField"
@remove-field="removeField"
@add-new-index="addNewIndex"
@add-to-index="addToIndex"
@rename-field="renameField"
/>
</div>
<WorkspacePropsOptionsModal
v-if="isOptionsModal"
:local-options="localOptions"
:table="table"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsIndexesModal
<WorkspaceTabPropsTableIndexesModal
v-if="isIndexesModal"
:local-indexes="localIndexes"
:table="table"
@@ -105,7 +161,7 @@
@hide="hideIndexesModal"
@indexes-update="indexesUpdate"
/>
<WorkspacePropsForeignModal
<WorkspaceTabPropsTableForeignModal
v-if="isForeignModal"
:local-key-usage="localKeyUsage"
:connection="connection"
@@ -125,19 +181,17 @@ import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
import WorkspacePropsForeignModal from '@/components/WorkspacePropsForeignModal';
import WorkspaceTabPropsTableFields from '@/components/WorkspaceTabPropsTableFields';
import WorkspaceTabPropsTableIndexesModal from '@/components/WorkspaceTabPropsTableIndexesModal';
import WorkspaceTabPropsTableForeignModal from '@/components/WorkspaceTabPropsTableForeignModal';
export default {
name: 'WorkspacePropsTab',
name: 'WorkspaceTabPropsTable',
components: {
BaseLoader,
WorkspacePropsTable,
WorkspacePropsOptionsModal,
WorkspacePropsIndexesModal,
WorkspacePropsForeignModal
WorkspaceTabPropsTableFields,
WorkspaceTabPropsTableIndexesModal,
WorkspaceTabPropsTableForeignModal
},
props: {
connection: Object,
@@ -149,7 +203,6 @@ export default {
return {
isLoading: false,
isSaving: false,
isOptionsModal: false,
isIndexesModal: false,
isForeignModal: false,
isOptionsChanging: false,
@@ -159,6 +212,7 @@ export default {
localKeyUsage: [],
originalIndexes: [],
localIndexes: [],
tableOptions: {},
localOptions: {},
lastTable: null,
newFieldsCounter: 0
@@ -176,12 +230,9 @@ export default {
tabUid () {
return this.$vnode.key;
},
tableOptions () {
const db = this.workspace.structure.find(db => db.name === this.schema);
return db && this.table ? db.tables.find(table => table.name === this.table) : {};
},
defaultEngine () {
return this.getDatabaseVariable(this.connection.uid, 'default_storage_engine').value || '';
const engine = this.getDatabaseVariable(this.connection.uid, 'default_storage_engine');
return engine ? engine.value : '';
},
schemaTables () {
const schemaTables = this.workspace.structure
@@ -237,6 +288,20 @@ export default {
renameTabs: 'workspaces/renameTabs',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async getTableOptions (params) {
const db = this.workspace.structure.find(db => db.name === this.schema);
if (db && db.tables.length && this.table)
this.tableOptions = db.tables.find(table => table.name === this.table);
else {
const { status, response } = await Tables.getTableOptions(params);
if (status === 'success')
this.tableOptions = response;
else
this.addNotification({ status: 'error', message: response });
}
},
async getFieldsData () {
if (!this.table) return;
@@ -244,10 +309,6 @@ export default {
this.lastTable = this.table;
this.newFieldsCounter = 0;
this.isLoading = true;
try {
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
}
catch (err) {}
const params = {
uid: this.connection.uid,
@@ -255,6 +316,14 @@ export default {
table: this.table
};
try {
await this.getTableOptions(params);
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
}
catch (err) {
console.error(err);
}
try { // Columns data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
@@ -527,6 +596,18 @@ export default {
return key;
});
},
duplicateField (uid) {
const fieldToClone = Object.assign({}, this.localFields.find(field => field._id === uid));
fieldToClone._id = uidGen();
fieldToClone.name = `${fieldToClone.name}_copy`;
fieldToClone.order = this.localFields.length + 1;
this.localFields = [...this.localFields, fieldToClone];
setTimeout(() => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20);
},
removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid);
},
@@ -548,12 +629,6 @@ export default {
return index;
});
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
optionsUpdate (options) {
this.localOptions = options;
},

View File

@@ -33,6 +33,9 @@
</div>
</div>
</div>
<div class="context-element" @click="duplicateField">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ $t('word.duplicate') }}</span>
</div>
<div class="context-element" @click="deleteField">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('message.deleteField') }}</span>
</div>
@@ -43,7 +46,7 @@
import BaseContextMenu from '@/components/BaseContextMenu';
export default {
name: 'WorkspaceQueryTableContext',
name: 'WorkspaceTabQueryTableContext',
components: {
BaseContextMenu
},
@@ -62,6 +65,10 @@ export default {
closeContext () {
this.$emit('close-context');
},
duplicateField () {
this.$emit('duplicate-selected');
this.closeContext();
},
deleteField () {
this.$emit('delete-selected');
this.closeContext();

View File

@@ -11,6 +11,7 @@
:index-types="indexTypes"
:indexes="indexes"
@delete-selected="removeField"
@duplicate-selected="duplicateField"
@close-context="isContext = false"
@add-new-index="$emit('add-new-index', $event)"
@add-to-index="$emit('add-to-index', $event)"
@@ -125,11 +126,11 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import Draggable from 'vuedraggable';
import TableRow from '@/components/WorkspacePropsTableRow';
import TableContext from '@/components/WorkspacePropsTableContext';
import TableRow from '@/components/WorkspaceTabPropsTableRow';
import TableContext from '@/components/WorkspaceTabPropsTableContext';
export default {
name: 'WorkspacePropsTable',
name: 'WorkspaceTabPropsTableFields',
components: {
TableRow,
TableContext,
@@ -220,6 +221,9 @@ export default {
this.contextEvent = event;
this.isContext = true;
},
duplicateField () {
this.$emit('duplicate-field', this.selectedField._id);
},
removeField () {
this.$emit('remove-field', this.selectedField._id);
},

View File

@@ -208,7 +208,7 @@ import Tables from '@/ipc-api/Tables';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsForeignModal',
name: 'WorkspaceTabPropsTableForeignModal',
components: {
ConfirmModal
},

View File

@@ -142,7 +142,7 @@ import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsIndexesModal',
name: 'WorkspaceTabPropsTableIndexesModal',
components: {
ConfirmModal
},

View File

@@ -1,12 +1,12 @@
<template>
<div class="tr" @contextmenu.prevent="$emit('contextmenu', $event, localRow._id)">
<div class="td" tabindex="0">
<div class="td p-0" tabindex="0">
<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>
<div class="td" tabindex="0">
<div class="td p-0" tabindex="0">
<div class="text-center">
<i
v-for="(index, i) in indexes"
@@ -23,7 +23,7 @@
/>
</div>
</div>
<div class="td" tabindex="0">
<div class="td p-0" tabindex="0">
<span
v-if="!isInlineEditor.name"
class="cell-content"
@@ -37,12 +37,12 @@
v-model="editingContent"
type="text"
autofocus
class="editable-field px-2"
class="editable-field form-input input-sm px-1"
@blur="editOFF"
>
</div>
<div
class="td text-uppercase"
class="td p-0 text-uppercase"
tabindex="0"
>
<span
@@ -57,7 +57,7 @@
v-else
ref="editField"
v-model="editingContent"
class="form-select editable-field small-select text-uppercase"
class="form-select editable-field pl-1 pr-4 small-select text-uppercase"
@blur="editOFF"
>
<option v-if="!isInDataTypes">
@@ -81,7 +81,7 @@
</div>
<div
v-if="customizations.tableArray"
class="td"
class="td p-0"
tabindex="0"
>
<label class="form-checkbox">
@@ -89,7 +89,7 @@
<i class="form-icon" />
</label>
</div>
<div class="td type-int" tabindex="0">
<div class="td p-0 type-int" tabindex="0">
<template v-if="fieldType.length">
<span
v-if="!isInlineEditor.length"
@@ -109,7 +109,7 @@
v-model="editingContent"
type="text"
autofocus
class="editable-field px-2"
class="editable-field form-input input-sm px-1"
@blur="editOFF"
>
<input
@@ -118,14 +118,14 @@
v-model="editingContent"
type="number"
autofocus
class="editable-field px-2"
class="editable-field form-input input-sm px-1"
@blur="editOFF"
>
</template>
</div>
<div
v-if="customizations.unsigned"
class="td"
class="td p-0"
tabindex="0"
>
<label class="form-checkbox">
@@ -139,7 +139,7 @@
</div>
<div
v-if="customizations.nullable"
class="td"
class="td p-0"
tabindex="0"
>
<label class="form-checkbox">
@@ -153,7 +153,7 @@
</div>
<div
v-if="customizations.zerofill"
class="td"
class="td p-0"
tabindex="0"
>
<label class="form-checkbox">
@@ -165,14 +165,14 @@
<i class="form-icon" />
</label>
</div>
<div class="td" tabindex="0">
<div class="td p-0" tabindex="0">
<span class="cell-content" @dblclick="editON($event, localRow.default, 'default')">
{{ fieldDefault }}
</span>
</div>
<div
v-if="customizations.comment"
class="td type-varchar"
class="td p-0 type-varchar"
tabindex="0"
>
<span
@@ -188,13 +188,13 @@
v-model="editingContent"
type="text"
autofocus
class="editable-field px-2"
class="editable-field form-input input-sm px-1"
@blur="editOFF"
>
</div>
<div
v-if="customizations.collation"
class="td"
class="td p-0"
tabindex="0"
>
<template v-if="fieldType.collation">
@@ -209,7 +209,7 @@
v-else
ref="editField"
v-model="editingContent"
class="form-select small-select editable-field"
class="form-select small-select pl-1 pr-4 editable-field"
@blur="editOFF"
>
<option
@@ -334,7 +334,7 @@ import { mapGetters } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsTableRow',
name: 'WorkspaceTabPropsTableRow',
components: {
ConfirmModal
},
@@ -459,7 +459,6 @@ export default {
: '';
}
else if (this.defaultValue.type === 'expression') {
console.log(this.localRow.default);
if (this.localRow.default.toUpperCase().includes('ON UPDATE'))
this.defaultValue.expression = this.localRow.default.replace(/ on update.*$/i, '');
else
@@ -576,12 +575,16 @@ export default {
position: absolute;
left: 0;
right: 0;
max-height: 21px;
border-radius: 3px;
font-size: 0.7rem;
}
.row-draggable {
position: relative;
text-align: right;
padding-left: 28px;
padding-right: 2px;
cursor: grab;
.row-draggable-icon {
@@ -603,13 +606,14 @@ export default {
min-height: auto;
.form-icon {
top: 0.15rem;
top: -0.65rem;
left: calc(50% - 8px);
}
}
.cell-content {
display: block;
padding: 0 0.2rem;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -31,7 +31,7 @@
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
@@ -71,49 +71,49 @@
</select>
</div>
</div>
</div>
<fieldset class="columns" :disabled="customizations.triggerOnlyRename">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label>
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label>
<div class="input-group">
<select v-model="localTrigger.activation" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select
v-if="!customizations.triggerMultipleEvents"
v-model="localTrigger.event"
class="form-select"
>
<option v-for="event in Object.keys(localEvents)" :key="event">
{{ event }}
<fieldset class="column columns mb-0" :disabled="customizations.triggerOnlyRename">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label>
<select v-model="localTrigger.table" class="form-select">
<option v-for="table in schemaTables" :key="table.name">
{{ table.name }}
</option>
</select>
<div v-if="customizations.triggerMultipleEvents" class="px-4">
<label
v-for="event in Object.keys(localEvents)"
:key="event"
class="form-checkbox form-inline"
@change.prevent="changeEvents(event)"
</div>
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label>
<div class="input-group">
<select v-model="localTrigger.activation" class="form-select">
<option>BEFORE</option>
<option>AFTER</option>
</select>
<select
v-if="!customizations.triggerMultipleEvents"
v-model="localTrigger.event"
class="form-select"
>
<input :checked="localEvents[event]" type="checkbox"><i class="form-icon" /> {{ event }}
</label>
<option v-for="event in Object.keys(localEvents)" :key="event">
{{ event }}
</option>
</select>
<div v-if="customizations.triggerMultipleEvents" class="px-4">
<label
v-for="event in Object.keys(localEvents)"
:key="event"
class="form-checkbox form-inline"
@change.prevent="changeEvents(event)"
>
<input :checked="localEvents[event]" type="checkbox"><i class="form-icon" /> {{ event }}
</label>
</div>
</div>
</div>
</div>
</div>
</fieldset>
</fieldset>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
@@ -137,7 +137,7 @@ import BaseLoader from '@/components/BaseLoader';
import Triggers from '@/ipc-api/Triggers';
export default {
name: 'WorkspacePropsTabTrigger',
name: 'WorkspaceTabPropsTrigger',
components: {
BaseLoader,
QueryEditor

View File

@@ -22,13 +22,60 @@
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<i class="mdi mdi-24px mdi-cogs mr-1" />
<span>{{ $t('word.options') }}</span>
</button>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div v-if="customizations.triggerFunctionlanguages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
</label>
<select v-model="localFunction.language" class="form-select">
<option v-for="language in customizations.triggerFunctionlanguages" :key="language">
{{ language }}
</option>
</select>
</div>
</div>
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
</label>
<select
v-if="workspace.users.length"
v-model="localFunction.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
</label>
<input
v-model="localFunction.comment"
class="form-input"
type="text"
>
</div>
</div>
</div>
@@ -44,13 +91,6 @@
:height="editorHeight"
/>
</div>
<WorkspacePropsTriggerFunctionOptionsModal
v-if="isOptionsModal"
:local-options="localFunction"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<ModalAskParameters
v-if="isAskingParameters"
:local-routine="localFunction"
@@ -66,16 +106,14 @@ import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsTriggerFunctionOptionsModal from '@/components/WorkspacePropsTriggerFunctionOptionsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Functions from '@/ipc-api/Functions';
export default {
name: 'WorkspacePropsTabTriggerFunction',
name: 'WorkspaceTabPropsTriggerFunction',
components: {
BaseLoader,
QueryEditor,
WorkspacePropsTriggerFunctionOptionsModal,
ModalAskParameters
},
props: {
@@ -88,7 +126,6 @@ export default {
return {
isLoading: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
@@ -106,6 +143,9 @@ export default {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
tabUid () {
return this.$vnode.key;
},
@@ -303,12 +343,6 @@ export default {
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true });
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},

View File

@@ -31,7 +31,7 @@
</div>
</div>
<div class="container">
<div class="columns mb-4">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
@@ -71,93 +71,35 @@
</select>
</div>
</div>
</div>
<div class="columns">
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="DEFINER"
>
<i class="form-icon" /> DEFINER
</label>
<label class="form-radio">
<input
v-model="localView.security"
type="radio"
name="security"
value="INVOKER"
>
<i class="form-icon" /> INVOKER
</label>
<select v-model="localView.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
name="algorithm"
value="UNDEFINED"
>
<i class="form-icon" /> UNDEFINED
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="MERGE"
name="algorithm"
>
<i class="form-icon" /> MERGE
</label>
<label class="form-radio">
<input
v-model="localView.algorithm"
type="radio"
value="TEMPTABLE"
name="algorithm"
>
<i class="form-icon" /> TEMPTABLE
</label>
<select v-model="localView.algorithm" class="form-select">
<option>UNDEFINED</option>
<option>MERGE</option>
<option>TEMPTABLE</option>
</select>
</div>
</div>
<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">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value=""
>
<i class="form-icon" /> None
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="CASCADED"
>
<i class="form-icon" /> CASCADED
</label>
<label class="form-radio">
<input
v-model="localView.updateOption"
type="radio"
name="update"
value="LOCAL"
>
<i class="form-icon" /> LOCAL
</label>
<select v-model="localView.updateOption" class="form-select">
<option value="">
None
</option>
<option>CASCADED</option>
<option>LOCAL</option>
</select>
</div>
</div>
</div>
@@ -184,7 +126,7 @@ import QueryEditor from '@/components/QueryEditor';
import Views from '@/ipc-api/Views';
export default {
name: 'WorkspacePropsTabView',
name: 'WorkspaceTabPropsView',
components: {
BaseLoader,
QueryEditor

View File

@@ -5,7 +5,8 @@
tabindex="0"
@keydown.116="runQuery(query)"
@keydown.ctrl.87="clear"
@keydown.ctrl.119="beautify"
@keydown.ctrl.66="beautify"
@keydown.ctrl.71="openHistoryModal"
>
<div class="workspace-query-runner column col-12">
<QueryEditor
@@ -32,16 +33,7 @@
<span>{{ $t('word.run') }}</span>
</button>
<button
class="btn btn-dark btn-sm"
:disabled="!query || isQuering"
title="CTRL+F8"
@click="beautify()"
>
<i class="mdi mdi-24px mdi-brush pr-1" />
<span>{{ $t('word.format') }}</span>
</button>
<button
class="btn btn-link btn-sm"
class="btn btn-link btn-sm mr-0"
:disabled="!query || isQuering"
title="CTRL+W"
@click="clear()"
@@ -52,6 +44,24 @@
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm"
:disabled="!query || isQuering"
title="CTRL+B"
@click="beautify()"
>
<i class="mdi mdi-24px mdi-brush pr-1" />
<span>{{ $t('word.format') }}</span>
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isQuering"
title="CTRL+G"
@click="openHistoryModal()"
>
<i class="mdi mdi-24px mdi-history pr-1" />
<span>{{ $t('word.history') }}</span>
</button>
<div class="dropdown table-dropdown pr-2">
<button
:disabled="!results.length || isQuering"
@@ -100,9 +110,10 @@
</div>
</div>
</div>
<WorkspaceTabQueryEmptyState v-if="!results.length && !isQuering" />
<div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" />
<WorkspaceQueryTable
<WorkspaceTabQueryTable
v-if="results"
v-show="!isQuering"
ref="queryTable"
@@ -115,24 +126,34 @@
@delete-selected="deleteSelected"
/>
</div>
<ModalHistory
v-if="isHistoryOpen"
:connection="connection"
@select-query="selectQuery"
@close="isHistoryOpen = false"
/>
</div>
</template>
<script>
import { format } from 'sql-formatter';
import { mapGetters, mapActions } from 'vuex';
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 WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable';
import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyState';
import ModalHistory from '@/components/ModalHistory';
import tableTabs from '@/mixins/tableTabs';
export default {
name: 'WorkspaceQueryTab',
name: 'WorkspaceTabQuery',
components: {
BaseLoader,
QueryEditor,
WorkspaceQueryTable
WorkspaceTabQueryTable,
WorkspaceTabQueryEmptyState,
ModalHistory
},
mixins: [tableTabs],
props: {
@@ -150,13 +171,15 @@ export default {
resultsCount: 0,
durationsCount: 0,
affectedCount: 0,
editorHeight: 200
editorHeight: 200,
isHistoryOpen: false
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace',
selectedWorkspace: 'workspaces/getSelected'
selectedWorkspace: 'workspaces/getSelected',
getHistoryByWorkspace: 'history/getHistoryByWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
@@ -172,6 +195,9 @@ export default {
},
isWorkspaceSelected () {
return this.workspace.uid === this.selectedWorkspace;
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
}
},
watch: {
@@ -186,7 +212,6 @@ export default {
created () {
this.query = this.tab.content;
this.selectedSchema = this.tab.schema || this.breadcrumbsSchema;
// this.changeBreadcrumbs({ schema: this.selectedSchema, query: `Query #${this.tab.index}` });
window.addEventListener('keydown', this.onKey);
},
@@ -210,7 +235,8 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
updateTabContent: 'workspaces/updateTabContent'
updateTabContent: 'workspaces/updateTabContent',
saveHistory: 'history/saveHistory'
}),
async runQuery (query) {
if (!query || this.isQuering) return;
@@ -233,7 +259,14 @@ export default {
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);
this.updateTabContent({ uid: this.connection.uid, tab: this.tab.uid, type: 'query', schema: this.selectedSchema, content: query });
this.updateTabContent({
uid: this.connection.uid,
tab: this.tab.uid,
type: 'query',
schema: this.selectedSchema,
content: query
});
this.saveHistory(params);
}
else
this.addNotification({ status: 'error', message: response });
@@ -292,6 +325,15 @@ export default {
this.$refs.queryEditor.editor.session.setValue(formattedQuery);
}
},
openHistoryModal () {
this.isHistoryOpen = true;
},
selectQuery (sql) {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue(sql);
this.isHistoryOpen = false;
},
clear () {
if (this.$refs.queryEditor)
this.$refs.queryEditor.editor.session.setValue('');

View File

@@ -0,0 +1,47 @@
<template>
<div class="container">
<div class="columns">
<div class="column col-16 text-right">
<div class="mb-4">
{{ $t('message.runQuery') }}
</div>
<div class="mb-4">
{{ $t('word.format') }}
</div>
<div class="mb-4">
{{ $t('word.clear') }}
</div>
<div class="mb-4">
{{ $t('word.history') }}
</div>
</div>
<div class="column col-16">
<div class="mb-4">
<code>F5</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>B</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>W</code>
</div>
<div class="mb-4">
<code>CTRL</code> + <code>G</code>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'WorkspaceTabQueryEmptyState'
};
</script>
<style scoped>
.container {
padding-top: 15vh;
opacity: 0.6;
}
</style>

View File

@@ -66,7 +66,7 @@
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<WorkspaceQueryTableRow
<WorkspaceTabQueryTableRow
v-for="row in items"
:key="row._id"
:row="row"
@@ -107,17 +107,17 @@ import { uidGen } from 'common/libs/uidGen';
import arrayToFile from '../libs/arrayToFile';
import { TEXT, LONG_TEXT, BLOB } from 'common/fieldTypes';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext';
import WorkspaceTabQueryTableRow from '@/components/WorkspaceTabQueryTableRow';
import TableContext from '@/components/WorkspaceTabQueryTableContext';
import ConfirmModal from '@/components/BaseConfirmModal';
import { mapActions, mapGetters } from 'vuex';
import moment from 'moment';
export default {
name: 'WorkspaceQueryTable',
name: 'WorkspaceTabQueryTable',
components: {
BaseVirtualScroll,
WorkspaceQueryTableRow,
WorkspaceTabQueryTableRow,
TableContext,
ConfirmModal
},
@@ -130,7 +130,7 @@ export default {
},
data () {
return {
resultsSize: 1000,
resultsSize: 0,
localResults: [],
isContext: false,
isDeleteConfirmModal: false,

View File

@@ -52,7 +52,7 @@
import BaseContextMenu from '@/components/BaseContextMenu';
export default {
name: 'WorkspaceQueryTableContext',
name: 'WorkspaceTabQueryTableContext',
components: {
BaseContextMenu
},

View File

@@ -11,7 +11,7 @@
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey] && fields[cKey]"
class="cell-content px-2"
class="cell-content"
:class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(fields[cKey].type.toLowerCase(), fields[cKey].length) | cutText }}</span>
@@ -31,7 +31,7 @@
v-mask="inputProps.mask"
:type="inputProps.type"
autofocus
class="editable-field px-2"
class="editable-field form-input input-sm px-1"
@blur="editOFF"
>
<select
@@ -59,7 +59,7 @@
v-model="editingContent"
:type="inputProps.type"
autofocus
class="editable-field px-2"
class="editable-field form-input input-sm px-1"
@blur="editOFF"
>
</template>
@@ -200,7 +200,7 @@ import TextEditor from '@/components/BaseTextEditor';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
name: 'WorkspaceQueryTableRow',
name: 'WorkspaceTabQueryTableRow',
components: {
ConfirmModal,
TextEditor,
@@ -528,6 +528,9 @@ export default {
border: none;
line-height: 1;
width: 100%;
max-height: 21px;
border-radius: 3px;
font-size: 0.7rem;
position: absolute;
left: 0;
right: 0;
@@ -535,6 +538,7 @@ export default {
.cell-content {
display: block;
padding: 0 0.2rem;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -112,8 +112,8 @@
<div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length | localeString }}</b>
</div>
<div v-if="hasApproximately || (page > 1 && tableInfo.rows)">
{{ $t('word.total') }}: <b>{{ tableInfo.rows | localeString }}</b> <small>({{ $t('word.approximately') }})</small>
<div v-if="hasApproximately || (page > 1 && approximateCount)">
{{ $t('word.total') }}: <b :title="$t('word.approximately')"> {{ approximateCount | localeString }}</b>
</div>
<div class="d-flex" :title="$t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
@@ -123,7 +123,7 @@
</div>
<div class="workspace-query-results p-relative column col-12">
<BaseLoader v-if="isQuering" />
<WorkspaceQueryTable
<WorkspaceTabQueryTable
v-if="results"
ref="queryTable"
:results="results"
@@ -159,23 +159,23 @@
<script>
import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable';
import ModalNewTableRow from '@/components/ModalNewTableRow';
import ModalFakerRows from '@/components/ModalFakerRows';
import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs';
export default {
name: 'WorkspaceTableTab',
name: 'WorkspaceTabTable',
components: {
BaseLoader,
WorkspaceQueryTable,
WorkspaceTabQueryTable,
ModalNewTableRow,
ModalFakerRows
},
filters: {
localeString (val) {
if (val)
if (val !== null)
return val.toLocaleString();
}
},
@@ -200,7 +200,8 @@ export default {
refreshInterval: null,
sortParams: {},
page: 1,
pageProxy: 1
pageProxy: 1,
approximateCount: 0
};
},
computed: {
@@ -232,15 +233,15 @@ export default {
hasApproximately () {
return this.results.length &&
this.results[0].rows &&
this.tableInfo &&
this.results[0].rows.length === this.limit &&
this.results[0].rows.length < this.tableInfo.rows;
this.results[0].rows.length < this.approximateCount;
}
},
watch: {
schema () {
if (this.isSelected) {
this.page = 1;
this.approximateCount = 0;
this.sortParams = {};
this.getTableData();
this.lastTable = this.table;
@@ -250,6 +251,7 @@ export default {
table () {
if (this.isSelected) {
this.page = 1;
this.approximateCount = 0;
this.sortParams = {};
this.getTableData();
this.lastTable = this.table;
@@ -285,7 +287,7 @@ export default {
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async getTableData () {
if (!this.table) return;
if (!this.table || !this.isSelected) return;
this.isQuering = true;
// if table changes clear cached values
@@ -315,6 +317,20 @@ export default {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.results.length && this.results[0].rows.length === this.limit) {
try { // Table approximate count
const { status, response } = await Tables.getTableApproximateCount(params);
if (status === 'success')
this.approximateCount = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
this.isQuering = false;
},
getTable () {

View File

@@ -115,7 +115,12 @@ module.exports = {
cell: 'Cell | Cells',
triggerFunction: 'Trigger function | Trigger functions',
all: 'All',
duplicate: 'Duplicate'
duplicate: 'Duplicate',
routine: 'Routine',
new: 'New',
history: 'History',
select: 'Select',
passphrase: 'Passphrase'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -227,7 +232,19 @@ module.exports = {
duplicateTable: 'Duplicate table',
noOpenTabs: 'There are no open tabs, navigate on the left bar or:',
noSchema: 'No schema',
restorePreviourSession: 'Restore previous session'
restorePreviourSession: 'Restore previous session',
runQuery: 'Run query',
thereAreNoTableFields: 'There are no table fields',
newTable: 'New table',
newView: 'New view',
newTrigger: 'New trigger',
newRoutine: 'New routine',
newFunction: 'New function',
newScheduler: 'New scheduler',
newTriggerFunction: 'New trigger function',
thereIsNoQueriesYet: 'There is no queries yet',
searchForQueries: 'Search for queries',
killProcess: 'Kill process'
},
faker: {
address: 'Address',

View File

@@ -11,7 +11,9 @@ const i18n = new VueI18n({
'es-ES': require('./es-ES'),
'fr-FR': require('./fr-FR'),
'pt-BR': require('./pt-BR'),
'de-DE': require('./de-DE')
'de-DE': require('./de-DE'),
'vi-VN': require('./vi-VN'),
'ja-JP': require('./ja-JP')
}
});
export default i18n;

View File

@@ -106,7 +106,16 @@ module.exports = {
array: 'Array',
changelog: 'Changelog',
format: 'Formatta',
sshTunnel: 'SSH tunnel'
sshTunnel: 'SSH tunnel',
structure: 'Structure',
small: 'Piccolo',
medium: 'Medio',
large: 'Largo',
row: 'Riga | Righe',
cell: 'Cella | Celle',
triggerFunction: 'Funzione di trigger | Funzioni di trigger',
all: 'Tutto',
duplicate: 'Duplica'
},
message: {
appWelcome: 'Benvenuto in Antares SQL Client!',
@@ -212,7 +221,13 @@ module.exports = {
deleteSchema: 'Elimina schema',
markdownSupported: 'Markdown supportato',
plantATree: 'Pianta un albero',
enableSsh: 'Abilita SSH'
dataTabPageSize: 'Grandezza pagina tab DATI',
enableSsh: 'Abilita SSH',
pageNumber: 'Numero pagina',
duplicateTable: 'Duplica tabella',
noOpenTabs: 'Non ci sono tab aperte, naviga nella barra sinistra o:',
noSchema: 'Nessuno schema',
restorePreviourSession: 'Ripristina sessione precedente'
},
faker: {
address: 'Indirizzo',

412
src/renderer/i18n/ja-JP.js Normal file
View File

@@ -0,0 +1,412 @@
module.exports = {
word: {
edit: '編集',
save: '保存',
close: '閉じる',
delete: '削除',
confirm: '確認',
cancel: 'キャンセル',
send: '送信',
connectionName: '接続名',
client: 'クライアント',
hostName: 'ホスト名',
port: 'ポート',
user: 'ユーザー名',
password: 'パスワード',
credentials: '認証情報',
connect: '接続',
connected: '接続中',
disconnect: '接続解除',
disconnected: '接続解除',
refresh: 'リフレッシュ',
settings: '設定',
general: '一般',
themes: 'テーマ',
update: '更新情報',
// about: 'お問い合わせ',
language: '言語',
version: 'バージョン',
donate: '寄付する',
run: '実行',
schema: 'スキーマ',
results: '結果',
size: 'サイズ',
seconds: '秒数',
type: 'タイプ',
mimeType: 'マイムタイプ',
download: 'ダウンロード',
add: '追加',
data: 'データ',
properties: 'プロパティ',
insert: '挿入',
connecting: '接続',
name: '名称',
collation: '照合',
clear: 'クリア',
options: 'オプション',
autoRefresh: 'オートリフレシュ',
indexes: 'インデックス',
foreignKeys: '外部キー',
length: '長さ',
unsigned: '符号なし',
default: 'デフォルト',
comment: 'コメント',
key: 'キー | キー',
order: '順序',
expression: '表現',
autoIncrement: 'オートインクリメント',
engine: 'エンジン',
field: 'フィールド | フィールド',
approximately: '約',
total: '合計',
table: 'テーブル',
discard: '破棄',
stay: 'ステイ',
author: '作者',
light: 'ライト',
dark: 'ダーク',
autoCompletion: 'オートコンプリート',
application: 'アプリケーション',
editor: 'エディター',
view: 'ビュー',
definer: 'デファイナー',
algorithm: 'アルゴリズム',
trigger: 'トリガー | トリガー',
storedRoutine: 'ストアド・ルーチン | ストアド・ルーチン',
scheduler: 'スケジューラー | スケジューラー',
event: 'イベント',
parameters: 'パラメータ',
function: '関数 | 関数',
// deterministic: '決定論的',
context: 'コンテキスト',
export: 'エクスポート',
returns: '戻り値',
timing: 'タイミング',
state: '状態',
execution: '実行',
starts: '開始',
ends: '終了',
ssl: 'SSL',
privateKey: '秘密鍵',
certificate: '証明書',
caCertificate: 'CA 証明書',
ciphers: '暗号',
upload: 'アップロード',
browse: '閲覧',
faker: 'フェイカー',
content: 'コンテンツ',
cut: 'カット',
copy: 'コピー',
paste: '貼り付け',
tools: 'ツール',
variables: '変数',
processes: 'プロセス',
database: 'データベース',
scratchpad: 'スクラッチパッド',
array: '配列',
changelog: '変更履歴',
format: 'フォーマット',
sshTunnel: 'SSH トンネル',
structure: '構造',
// small: '小規模',
// medium: '中型',
// large: 'ラージ',
row: 'ロウ | ロウ',
cell: 'セル | セル',
triggerFunction: 'トリガー関数 | トリガー関数',
all: 'すべて',
duplicate: 'デュプリケート',
routine: 'ルーチン',
// new: '新機能',
history: '履歴',
select: '選択'
},
message: {
appWelcome: 'Antares SQL Client へようこそ!',
appFirstStep: '最初のステップは、新しいデータベース接続を作成することです。',
addConnection: '接続の追加',
createConnection: '接続の作成',
createNewConnection: '新しい接続の作成',
askCredentials: '認証情報の入力',
testConnection: '接続のテスト',
editConnection: '接続の編集',
deleteConnection: '接続の削除',
deleteCorfirm: 'のキャンセルを確認しますか?',
connectionSuccessfullyMade: '接続に成功しました。',
madeWithJS: '💛 と JavaScript で作られています。',
checkForUpdates: '更新情報の確認',
noUpdatesAvailable: 'アップデートがありません',
checkingForUpdate: 'アップデートを確認中',
checkFailure: 'チェックに失敗しました、後で試してください',
updateAvailable: 'アップデートが利用可能です',
downloadingUpdate: 'アップデートのダウンロード',
updateDownloaded: 'アップデートのダウンロード',
restartToInstall: 'Antares を再起動してインストールしてください',
unableEditFieldWithoutPrimary: '主キーのないフィールドを結果セットで編集できない',
editCell: 'セルの編集',
deleteRows: '行の削除 | {count} 行の削除',
confirmToDeleteRows: '1つの行を削除することを確認しますか | {count} 行を削除することを確認しますか?',
notificationsTimeout: '通知のタイムアウト',
uploadFile: 'ファイルのアップロード',
addNewRow: '新しい行の追加',
numberOfInserts: 'インサート数',
openNewTab: '新しいタブを開く',
affectedRows: '影響を受ける行',
createNewDatabase: '新規データベースの作成',
databaseName: 'データベース名',
serverDefault: 'サーバーのデフォルト',
deleteDatabase: 'データベースの削除',
editDatabase: 'データベースの編集',
clearChanges: '変更の消去',
addNewField: '新しいフィールドの追加',
manageIndexes: 'インデックスの管理',
manageForeignKeys: '外部キーの管理',
allowNull: 'NULL を許可する',
zeroFill: 'ゼロフィル',
customValue: 'カスタム値',
onUpdate: '更新時',
deleteField: 'フィールドの削除',
createNewIndex: '新しいインデックスの作成',
addToIndex: 'インデックスへの追加',
createNewTable: '新しいテーブルの作成',
emptyTable: '空のテーブル',
deleteTable: 'テーブルの削除',
emptyCorfirm: '空にすることを確認しますか?',
unsavedChanges: '保存されていない変更',
discardUnsavedChanges: '保存されていない変更があります。このタブを閉じると、これらの変更は破棄されます。',
thereAreNoIndexes: 'インデックスがありません',
thereAreNoForeign: '外部キーがありません。',
createNewForeign: '新しい外部キーの作成',
referenceTable: '参照テーブル',
referenceField: '参照フィールド',
foreignFields: '外部フィールド',
invalidDefault: '無効なデフォルト',
onDelete: '削除時',
applicationTheme: 'アプリケーションテーマ',
editorTheme: 'エディターテーマ',
wrapLongLines: '長い行の折り返し',
selectStatement: '選択文',
triggerStatement: 'トリガー文',
sqlSecurity: 'SQL セキュリティ',
updateOption: '更新オプション',
deleteView: 'ビューの削除',
createNewView: '新規ビューの作成',
deleteTrigger: 'トリガーの削除',
createNewTrigger: '新しいトリガの作成',
currentUser: '現在のユーザー',
routineBody: 'ルーチン本体',
dataAccess: 'データアクセス',
thereAreNoParameters: 'パラメータはありません',
createNewParameter: '新しいパラメータの作成',
createNewRoutine: 'ストアド・ルーチンの新規作成',
deleteRoutine: 'ストアド・ルーチンの削除',
functionBody: '関数本体',
createNewFunction: '新しい関数の作成',
deleteFunction: '関数の削除',
schedulerBody: 'スケジューラ本体',
createNewScheduler: 'スケジューラの新規作成',
deleteScheduler: 'スケジューラの削除',
preserveOnCompletion: '完了時に保存する',
enableSsl: 'SSL 対応',
manualValue: 'マニュアル値',
tableFiller: 'テーブルフィラー',
fakeDataLanguage: 'フェイクデータの言語',
searchForElements: '要素の検索',
selectAll: 'すべてを選択する',
queryDuration: '問い合わせ期間',
includeBetaUpdates: 'ベータ版アップデートを含む',
setNull: 'NULL の設定',
processesList: 'プロセス一覧',
processInfo: 'プロセス情報',
manageUsers: 'ユーザーの管理',
createNewSchema: '新しいスキーマの作成',
schemaName: 'スキーマ名',
editSchema: 'スキーマの編集',
deleteSchema: 'スキーマの削除',
markdownSupported: 'マークダウン対応',
// plantATree: '木を植える',
dataTabPageSize: 'DATA タブのページサイズ',
enableSsh: 'SSH を有効にする',
pageNumber: 'ページ番号',
duplicateTable: 'テーブルを複製する',
noOpenTabs: '開いているタブがありません。左のバーでナビゲートするか',
noSchema: 'スキーマなし',
restorePreviourSession: '前のセッションに戻す',
runQuery: 'クエリの実行',
thereAreNoTableFields: 'テーブルのフィールドがありません',
newTable: '新しいテーブル',
newView: '新しいビュー',
newTrigger: '新しいトリガー',
newRoutine: '新しいルーチン',
newFunction: '新しい関数',
newScheduler: '新規スケジューラ',
newTriggerFunction: '新しいトリガー機能',
thereIsNoQueriesYet: 'まだ問い合わせはありません',
searchForQueries: 'クエリの検索',
killProcess: 'プロセスの停止'
},
faker: {
address: '住所',
commerce: 'コマース',
company: '会社名',
database: 'データベース',
date: '日付',
finance: 'ファイナンス',
// git: 'ギット',
hacker: 'ハッカー',
internet: 'インターネット',
// lorem: 'ローレム',
name: '名前',
music: '音楽',
phone: '電話',
random: 'ランダム',
system: 'システム',
time: '時間',
vehicle: '車',
zipCode: '郵便番号',
zipCodeByState: '都道府県別郵便番号',
city: '都市名',
cityPrefix: '市のプレフィックス',
citySuffix: '市の接尾辞',
streetName: '通りの名前',
streetAddress: 'ストリートアドレス',
streetSuffix: '通りの接尾辞',
streetPrefix: 'ストリートプレフィックス',
secondaryAddress: '副住所',
county: '郡',
country: '国名',
countryCode: '国コード',
state: '州',
stateAbbr: '州の略語',
latitude: '緯度',
longitude: '経度',
direction: '方向',
cardinalDirection: '枢機卿の方向',
ordinalDirection: '序列方向',
nearbyGPSCoordinate: '近くのGPS座標',
timeZone: 'タイムゾーン',
color: '色',
department: '部門',
productName: '商品名',
price: '価格',
productAdjective: '製品の形容詞',
productMaterial: '製品の素材',
product: '製品',
productDescription: '製品の説明',
suffixes: 'サフィックス',
companyName: '会社名',
companySuffix: '会社のサフィックス',
catchPhrase: 'キャッチフレーズ',
// bs: 'BS',
catchPhraseAdjective: 'キャッチフレーズ形容詞',
catchPhraseDescriptor: 'キャッチフレーズの説明文',
catchPhraseNoun: 'キャッチフレーズの名詞',
bsAdjective: 'BS 形容詞',
bsBuzz: 'BS の話題',
bsNoun: 'BS の名詞',
column: 'コラム',
type: 'タイプ',
collation: '照合',
engine: 'エンジン',
past: '過去',
future: '未来',
between: '間',
recent: '最近',
soon: 'すぐ',
month: '月',
weekday: '曜日',
account: 'アカウント',
accountName: '口座名',
routingNumber: 'ルーティング番号',
mask: 'マスク',
amount: '金額',
transactionType: '取引の種類',
currencyCode: '通貨コード',
currencyName: '通貨名',
currencySymbol: '通貨記号',
bitcoinAddress: 'Bitcoin アドレス',
litecoinAddress: 'ライトコインのアドレス',
creditCardNumber: 'クレジットカード番号',
creditCardCVV: 'クレジットカードの CVV',
ethereumAddress: 'イーサリアムのアドレス',
iban: 'アイバン',
bic: 'ビック',
transactionDescription: '取引内容',
branch: 'ブランチ',
commitEntry: 'コミットエントリ',
commitMessage: 'コミットメッセージ',
commitSha: 'コミット SHA',
shortSha: 'ショート SHA',
abbreviation: '省略形',
adjective: '形容詞',
noun: '名詞',
verb: '動詞',
ingverb: '動詞',
phrase: 'フレーズ',
avatar: 'アバター',
email: 'メール',
exampleEmail: 'メールの例',
userName: 'ユーザー名',
protocol: 'プロトコル',
url: 'URL',
domainName: 'ドメイン名',
domainSuffix: 'ドメインのサフィックス',
domainWord: 'ドメイン名',
ip: 'Ip',
ipv6: 'Ipv6',
userAgent: 'ユーザーエージェント',
// mac: 'Mac',
password: 'パスワード',
word: 'ワード',
words: '単語',
sentence: '文章',
slug: 'スラッグ',
sentences: 'センテンス',
paragraph: 'パラグラフ',
paragraphs: 'パラグラフ',
text: 'テキスト',
lines: '行',
genre: 'ジャンル',
firstName: 'ファーストネーム',
lastName: '苗字',
middleName: 'ミドルネーム',
findName: 'フルネーム',
jobTitle: '役職名',
gender: '性別',
prefix: 'プレフィックス',
suffix: 'サフィックス',
title: '役職名',
jobDescriptor: '職務記述書',
jobArea: '職務領域',
jobType: '仕事の種類',
phoneNumber: '電話番号',
phoneNumberFormat: '電話番号のフォーマット',
phoneFormats: '電話番号のフォーマット',
// number: '番号',
// float: 'フロート',
arrayElement: '配列要素',
arrayElements: '配列要素',
objectElement: 'オブジェクトの要素',
// uuid: 'Uuid',
// boolean: 'ブール',
image: '画像',
locale: 'ロケール',
alpha: '英字',
alphaNumeric: '英数字',
hexaDecimal: '16進法',
fileName: 'ファイル名',
commonFileName: '一般的なファイル名',
mimeType: 'Mimeタイプ',
commonFileType: '共通のファイルタイプ',
commonFileExt: '共通のファイル拡張子',
fileType: 'ファイルタイプ',
fileExt: 'ファイル拡張子',
directoryPath: 'ディレクトリパス',
filePath: 'ファイルパス',
// semver: 'セムバー',
manufacturer: 'メーカー名',
model: 'モデル',
fuel: '燃料'
// vin: 'Vin'
}
};

View File

@@ -5,5 +5,7 @@ export default {
'es-ES': 'Español',
'fr-FR': 'Français',
'pt-BR': 'Português (Brasil)',
'de-DE': 'Deutsch (Deutschland)'
'de-DE': 'Deutsch (Deutschland)',
'vi-VN': 'Tiếng Việt',
'ja-JP': '日本語'
};

412
src/renderer/i18n/vi-VN.js Normal file
View File

@@ -0,0 +1,412 @@
module.exports = {
word: {
edit: 'Chỉnh sửa',
save: 'Lưu',
close: 'Đóng',
delete: 'Xoá',
confirm: 'Xác nhận',
cancel: 'Huỷ',
send: 'Gửi',
connectionName: 'Tên kết nối',
client: 'Client',
hostName: 'Tên máy chủ',
port: 'Cổng',
user: 'Người dùng',
password: 'Mật khẩu',
credentials: 'Thông tin xác thực',
connect: 'Kết nối',
connected: 'Đã kết nối',
disconnect: 'Ngắt kết nối',
disconnected: 'Đã ngắt kết nối',
refresh: 'Làm mới',
settings: 'Cài đặt',
general: 'Chung',
themes: 'Giao diện',
update: 'Cập nhật',
about: 'Giới thiệu',
language: 'Ngôn ngữ',
version: 'Phiên bản',
donate: 'Ủng hộ',
run: 'Chạy',
schema: 'Schema',
results: 'Kết quả',
size: 'Kích thước',
seconds: 'Giây',
type: 'Kiểu',
mimeType: 'Mime-Type',
download: 'Tải xuống',
add: 'Thêm',
data: 'Dữ liệu',
properties: 'Thuộc tính',
insert: 'Nhập',
connecting: 'Đang kết nối',
name: 'Tên',
collation: 'Đối chiếu',
clear: 'Xoá',
options: 'Tuỳ chọn',
autoRefresh: 'Tự động làm mới',
indexes: 'Index',
foreignKeys: 'Khoá ngoại',
length: 'Độ dài',
unsigned: 'Unsigned',
default: 'Mặc định',
comment: 'Nhận xét',
key: 'Khoá | Khoá',
order: 'Sắp xếp',
expression: 'Biểu hiện',
autoIncrement: 'Tự động tăng',
engine: 'Engine',
field: 'Trường | Trường',
approximately: 'Khoảng',
total: 'Toàn bộ',
table: 'Bảng',
discard: 'Bỏ',
stay: 'Ở lại',
author: 'Tác giả',
light: 'Sáng',
dark: 'Tối',
autoCompletion: 'Tự động hoàn thành',
application: 'Ứng dụng',
editor: 'Người chỉnh sửa',
view: 'Xem',
definer: 'Định nghĩa',
algorithm: 'Thuật toán',
trigger: 'Kích hoạt | Kích hoạt',
storedRoutine: 'Quy trình đã lưu | Quy trình đã lưu',
scheduler: 'Lập lịch trình | Lập lịch trình',
event: 'Sự kiện',
parameters: 'Tham số',
function: 'Chức năng | Chức năng',
deterministic: 'Xác định',
context: 'Context',
export: 'Xuất',
returns: 'Returns',
timing: 'Thời gian',
state: 'Trạng thái',
execution: 'Thực thi',
starts: 'Bắt đầu',
ends: 'Kết thúc',
ssl: 'SSL',
privateKey: 'Mã khoá riêng tư',
certificate: 'Chứng chỉ',
caCertificate: 'Chứng chỉ CA',
ciphers: 'Ciphers',
upload: 'Tải lên',
browse: 'Duyệt',
faker: 'Faker',
content: 'Nội dung',
cut: 'Cắt',
copy: 'Sao chép',
paste: 'Dán',
tools: 'Công cụ',
variables: 'Biến',
processes: 'Quá trình',
database: 'Cơ sở dữ liệu',
scratchpad: 'Scratchpad',
array: 'Mảng',
changelog: 'Nhật ký thay đổi',
format: 'Định dạng',
sshTunnel: 'SSH tunnel',
structure: 'Structure',
small: 'Nhỏ',
medium: 'Vừa',
large: 'Lớn',
row: 'Hàng | Hàng',
cell: 'Ô | Ô',
triggerFunction: 'Trigger function | Trigger functions',
all: 'Tất cả',
duplicate: 'Bản sao',
routine: 'Routine',
new: 'Mới',
history: 'Lịch sử',
select: 'Chọn'
},
message: {
appWelcome: 'Chào bạn đến với Antares SQL Client!',
appFirstStep: 'Bước đầu tiên: tạo một kết nối tới cơ sở dữ liệu.',
addConnection: 'Thêm kết nối',
createConnection: 'Tạo kết nối',
createNewConnection: 'Tạo kết nối mới',
askCredentials: 'Yêu cầu thông tin đăng nhập',
testConnection: 'Chạy thử kết nối',
editConnection: 'Sửa kết nối',
deleteConnection: 'Xoá kết nối',
deleteCorfirm: 'Bạn có xác nhận việc hủy bỏ',
connectionSuccessfullyMade: 'Kết nối được thực hiện thành công!',
madeWithJS: 'Được tạo bằng 💛 và JavaScript!',
checkForUpdates: 'Kiểm tra cập nhật',
noUpdatesAvailable: 'Không có bản cập nhật nào',
checkingForUpdate: 'Đang kiểm tra cập nhật',
checkFailure: 'Kiểm tra thất bại, vui lòng thử lại sau',
updateAvailable: 'Cập nhật có sẵn',
downloadingUpdate: 'Đang tải bản cập nhật',
updateDownloaded: 'Đã tải bản cập nhạt',
restartToInstall: 'Khởi động lại Antares để cài đặt',
unableEditFieldWithoutPrimary: 'Không thể chỉnh sửa trường mà không có khóa chính trong kết quả',
editCell: 'Sửa ô',
deleteRows: 'Xoá hàng | Xoá {count} hàng',
confirmToDeleteRows: 'Bạn có xác nhận xóa một hàng không? | Bạn có xác nhận xóa {count} hàng không?',
notificationsTimeout: 'Thông báo hết giờ',
uploadFile: 'Tải lên tệp',
addNewRow: 'Thêm hàng mới',
numberOfInserts: 'Số lần nhập',
openNewTab: 'Mở trong tab mới',
affectedRows: 'Các hàng bị ảnh hưởng',
createNewDatabase: 'Tạo Cơ sở dữ liệu mới',
databaseName: 'Tên cơ sở dữ liệu',
serverDefault: 'Máy chủ mặc định',
deleteDatabase: 'Xoá cơ sở dữ liệu',
editDatabase: 'Sửa cơ sở dữ liệu',
clearChanges: 'Xóa các thay đổi',
addNewField: 'Thêm trường mới',
manageIndexes: 'Quản lý index',
manageForeignKeys: 'Quản lý khoá ngoại',
allowNull: 'Cho phép NULL',
zeroFill: 'Không điền',
customValue: 'Tuỳ chỉnh giá trị',
onUpdate: 'Đang cập nhật',
deleteField: 'Xoá trường',
createNewIndex: 'Tạo index mới',
addToIndex: 'Thêm vào index',
createNewTable: 'Tạo bảng mới',
emptyTable: 'Bảng trống',
deleteTable: 'Xoá bảng',
emptyCorfirm: 'Bạn có xác nhận để làm trống không',
unsavedChanges: 'Chưa lưu lại thay đổi',
discardUnsavedChanges: 'Bạn có một số thay đổi chưa được lưu. Đóng tab này, những thay đổi này sẽ bị huỷ bỏ.',
thereAreNoIndexes: 'Không có index',
thereAreNoForeign: 'Không có khoá ngoại',
createNewForeign: 'Tạo khoá ngoại mới',
referenceTable: 'Tham khảo bảng',
referenceField: 'Tham khảo trường',
foreignFields: 'Trường ngoại',
invalidDefault: 'Mặc định không hợp lệ',
onDelete: 'Đang xoá',
applicationTheme: 'Chủ đề ứng dụng',
editorTheme: 'Trình chỉnh sửa chủ đề',
wrapLongLines: 'Wrap long lines',
selectStatement: 'Chọn câu lệnh',
triggerStatement: 'Kích hoạt câu lệnh',
sqlSecurity: 'Bảo mật SQL',
updateOption: 'Cập nhật tuỳ chọn',
deleteView: 'Xóa chế độ xem',
createNewView: 'Tạo chế độ xem mới',
deleteTrigger: 'Xóa trình kích hoạt',
createNewTrigger: 'Tạo trình kích hoạt mới',
currentUser: 'Người dùng hiện tại',
routineBody: 'Body quy trình',
dataAccess: 'Truy cập dữ liệu',
thereAreNoParameters: 'Không có tham số',
createNewParameter: 'Tạo tham số mới',
createNewRoutine: 'Tạo quy trình lưu trữ mới',
deleteRoutine: 'Xoá quy trình lưu trữ',
functionBody: 'Body chức năng',
createNewFunction: 'Tạo chức năng mới',
deleteFunction: 'Xoá chức năng',
schedulerBody: 'Body trình lập lịch',
createNewScheduler: 'Tạo lịch trình mới',
deleteScheduler: 'Xóa trình lên lịch',
preserveOnCompletion: 'Bảo tồn khi hoàn thành',
enableSsl: 'Bật SSL',
manualValue: 'Giá trị thủ công',
tableFiller: 'Bộ lọc bảng',
fakeDataLanguage: 'Ngôn ngữ dữ liệu giả mạo',
searchForElements: 'Tìm kiếm yếu tố',
selectAll: 'Chọn tất cả',
queryDuration: 'Thời lượng truy vấn',
includeBetaUpdates: 'Bao gồm các bản cập nhật beta',
setNull: 'Đặt NULL',
processesList: 'Danh sách quy trình',
processInfo: 'Thông tin quá trình',
manageUsers: 'Quản lý người dùng',
createNewSchema: 'Tạo schema mới',
schemaName: 'Tên schema',
editSchema: 'Sửa schema',
deleteSchema: 'Xoá schema',
markdownSupported: 'Hỗ trợ Markdown',
plantATree: 'Trồng cây',
dataTabPageSize: 'Kích thước trang tab DATA',
enableSsh: 'Bật SSH',
pageNumber: 'Số trang',
duplicateTable: 'Sao chép bản',
noOpenTabs: 'Không có tab nào đang mở, điều hướng trên thanh bên trái hoặc:',
noSchema: 'Không có schema',
restorePreviourSession: 'Khôi phục phiên trước',
runQuery: 'Chạy truy vấn',
thereAreNoTableFields: 'Không có trường bảng',
newTable: 'Bảng mới',
newView: 'Chế độ xem mới',
newTrigger: 'Trình kích hoạt mới',
newRoutine: 'Quy trình mới',
newFunction: 'Chức năng mới',
newScheduler: 'Bộ lập lịch mới',
newTriggerFunction: 'Chức năng kích hoạt mới',
thereIsNoQueriesYet: 'Không có truy vấn nào',
searchForQueries: 'Tìm kiếm truy vấn',
killProcess: 'Huỷ quá trình'
},
faker: {
address: 'Địa chỉ',
commerce: 'Thương mại',
company: 'Công ty',
database: 'Cơ sở dữ liệu',
date: 'Ngày',
finance: 'Tài chánh',
git: 'Git',
hacker: 'Tin tặc',
internet: 'Mạng Internet',
lorem: 'Lorem',
name: 'Tên',
music: 'Âm nhạc',
phone: 'Số điện thoại',
random: 'Ngẫu nhiên',
system: 'Hệ thống',
time: 'Thời gian',
vehicle: 'Phương tiện giao thông',
zipCode: 'Mã Bưu Chính',
zipCodeByState: 'Mã Bưu Chính theo tiểu bang',
city: 'Thành phố',
cityPrefix: 'Tiền tố thành phố',
citySuffix: 'Hậu tố thành phố',
streetName: 'Tên đường',
streetAddress: 'Địa chỉ đường',
streetSuffix: 'Hậu tố đường',
streetPrefix: 'Tiền tố đường',
secondaryAddress: 'Địa chỉ phụ',
county: 'Quận',
country: 'Quốc gia',
countryCode: 'Mã quốc gia',
state: 'Tiểu bang',
stateAbbr: 'Viết tắt của tiểu bang',
latitude: 'Vĩ độ',
longitude: 'Kinh độ',
direction: 'Hướng',
cardinalDirection: 'Hướng cốt yếu',
ordinalDirection: 'Hướng thứ tự',
nearbyGPSCoordinate: 'Tọa độ GPS lân cận',
timeZone: 'Múi giờ',
color: 'Màu',
department: 'Phòng',
productName: 'Tên sản phẩm',
price: 'Giá',
productAdjective: 'Tính từ sản phẩm',
productMaterial: 'Chất liệu sản phẩm',
product: 'Sản phẩm',
productDescription: 'Mô tả sản phẩm',
suffixes: 'Hậu tố',
companyName: 'Tên công ty',
companySuffix: 'Hậu tố công ty',
catchPhrase: 'Khẩu hiệu',
bs: 'BS',
catchPhraseAdjective: 'Bắt cụm từ tính từ',
catchPhraseDescriptor: 'Bắt bộ mô tả cụm từ',
catchPhraseNoun: 'Bắt cụm từ danh từ',
bsAdjective: 'BS tính từ',
bsBuzz: 'BS buzz',
bsNoun: 'BS danh từ',
column: 'Cột',
type: 'Loại',
collation: 'Đối chiếu',
engine: 'Engine',
past: 'Quá khứ',
future: 'Tương lai',
between: 'Giữa',
recent: 'Gần đây',
soon: 'Sớm',
month: 'Tháng',
weekday: 'Ngày trong tuần',
account: 'Tài khoản',
accountName: 'Tên tài khoản',
routingNumber: 'Số định tuyến',
mask: 'Mặt nạ',
amount: 'Số tiền',
transactionType: 'Loại giao dịch',
currencyCode: 'Mã tiền tệ',
currencyName: 'Tên tiền tệ',
currencySymbol: 'Ký hiệu tiền tệ',
bitcoinAddress: 'Địa chỉ Bitcoin',
litecoinAddress: 'Địa chỉ Litecoin',
creditCardNumber: 'Số thẻ tín dụng',
creditCardCVV: 'CVV thẻ tín dụng',
ethereumAddress: 'Địa chỉ Ethereum',
iban: 'Iban',
bic: 'Bic',
transactionDescription: 'Mô tả giao dịch',
branch: 'Nhánh',
commitEntry: 'Nhập cam kết',
commitMessage: 'Thông báo cam kết',
commitSha: 'Cam kết SHA',
shortSha: 'SHA ngắn',
abbreviation: 'Viết tắt',
adjective: 'Tính từ',
noun: 'Danh từ',
verb: 'Động từ',
ingverb: 'Động từ ing',
phrase: 'Cụm từ',
avatar: 'Ảnh đại diện',
email: 'Email',
exampleEmail: 'Email ví dụ',
userName: 'Tên người dùng',
protocol: 'Giao thức',
url: 'Url',
domainName: 'Tên miền',
domainSuffix: 'Hậu tố miền',
domainWord: 'Từ miền',
ip: 'Ip',
ipv6: 'Ipv6',
userAgent: 'User agent',
mac: 'Mac',
password: 'Mật khẩu',
word: 'Từ',
words: 'Từ',
sentence: 'Câu',
slug: 'Slug',
sentences: 'Câu',
paragraph: 'Đoạn văn',
paragraphs: 'Đoạn văn',
text: 'Văn bản',
lines: 'Dòng',
genre: 'Thể loại',
firstName: 'Tên',
lastName: 'Họ',
middleName: 'Tên đệm',
findName: 'Tên đầy đủ',
jobTitle: 'Chức vụ',
gender: 'Giới tính',
prefix: 'Tiền tố',
suffix: 'Hậu tố',
title: 'Tiêu đề',
jobDescriptor: 'Mô tả công việc',
jobArea: 'Lĩnh vực việc làm',
jobType: 'Loại công việc',
phoneNumber: 'Số điện thoại',
phoneNumberFormat: 'Định dạng số điện thoại',
phoneFormats: 'Định dạng điện thoại',
number: 'Số',
float: 'Float',
arrayElement: 'Phân tử array',
arrayElements: 'Phân tử array',
objectElement: 'Phần tử object',
uuid: 'Uuid',
boolean: 'Boolean',
image: 'Hình ảnh',
locale: 'Ngôn ngữ',
alpha: 'Alpha',
alphaNumeric: 'Chữ và số',
hexaDecimal: 'Hệ thập lục phân',
fileName: 'File name',
commonFileName: 'Tên tệp chung',
mimeType: 'Kiểu mine',
commonFileType: 'Loại tệp chung',
commonFileExt: 'Phần mở rộng tệp chung',
fileType: 'Loại tệp',
fileExt: 'Phần mở rộng tệp',
directoryPath: 'Đường dẫn thư mục',
filePath: 'Đường dẫn tệp',
semver: 'Semver',
manufacturer: 'Manufacturer',
model: 'Model',
fuel: 'Fuel',
vin: 'Vin'
}
};

View File

@@ -42,6 +42,10 @@ export default class {
return ipcRenderer.invoke('get-processes', uid);
}
static killProcess (params) {
return ipcRenderer.invoke('kill-process', params);
}
static useSchema (params) {
return ipcRenderer.invoke('use-schema', params);
}

View File

@@ -10,6 +10,14 @@ export default class {
return ipcRenderer.invoke('get-table-data', params);
}
static getTableApproximateCount (params) {
return ipcRenderer.invoke('get-table-count', params);
}
static getTableOptions (params) {
return ipcRenderer.invoke('get-table-options', params);
}
static getTableIndexes (params) {
return ipcRenderer.invoke('get-table-indexes', params);
}

View File

@@ -29,6 +29,7 @@
.th {
padding: $unit-3 $unit-2;
display: table-cell;
border-radius: 3px;
}
.th {

View File

@@ -17,7 +17,7 @@
&.key-mul,
&.key-INDEX,
&.key-KEY {
color: palegreen;
color: limegreen;
}
&.key-FULLTEXT {

View File

@@ -15,6 +15,15 @@ body {
user-select: none;
}
::selection,
option:hover,
option:focus,
option:active,
option:checked {
background-color: $primary-color;
color: $light-color;
}
/* Additions */
@include margin-variant(3, $unit-3);
@include margin-variant(4, $unit-4);
@@ -200,7 +209,7 @@ body {
cursor: pointer;
&.small-select {
height: 1rem;
height: 21px;
font-size: 0.7rem;
padding: 1px 0.4rem 0;
}

View File

@@ -20,6 +20,11 @@
}
}
option,
optgroup {
background-color: $bg-color-gray;
}
// Override Spectre.css
.menu {
background: $bg-color-light-dark;
@@ -135,6 +140,11 @@
background: $bg-color-light-dark;
}
code {
background-color: #000;
color: $body-font-color-dark;
}
// Antares
.workspace {
.workspace-explorebar {
@@ -162,7 +172,8 @@
}
.menu-item {
&:hover {
&:hover,
&.selected {
color: $body-font-color-dark;
background: rgba($color: #fff, $alpha: 0.05);
}
@@ -173,16 +184,29 @@
.workspace-query-results {
.table {
.th {
background: $bg-color-dark;
border-color: $bg-color-light-dark;
border-color: darken($bg-color-light-gray, 80%);
background-color: $body-bg-dark;
}
.td {
border-color: $bg-color-light-dark;
.tr {
background-color: darken($bg-color-light-gray, 80%);
&:focus {
box-shadow: inset 0 0 0 1px $body-font-color-dark;
background: rgba($color: #000, $alpha: 0.3);
.td:first-child {
border-left: 2px solid $body-bg-dark;
}
.td {
border-color: $body-bg-dark;
&:focus {
box-shadow: inset 0 0 0 2px darken($body-font-color-dark, 40%);
background-color: rgba($color: #000, $alpha: 0.3);
}
.editable-field {
box-shadow: inset 0 0 0 2px darken($body-font-color-dark, 40%);
background-color: rgba($color: #000, $alpha: 0.3);
}
}
}
}
@@ -259,6 +283,12 @@
}
.tile {
transition: background 0.2s;
&:focus {
background: rgba($bg-color-light-dark, 60%);
}
&:hover {
background: $bg-color-light-dark;
}

View File

@@ -18,20 +18,11 @@
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;
}
.menu {
.menu-item a {
&:hover {
color: $body-font-color;
background: rgba($color: #000, $alpha: 0.1);
}
}
}
@@ -88,6 +79,12 @@
}
.tile {
transition: background 0.2s;
&:focus {
background: rgba($bg-color-light-gray, 70%);
}
&:hover {
background: $bg-color-light-gray;
}
@@ -111,6 +108,24 @@
}
}
#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;
}
}
}
}
#settingbar {
width: $settingbar-width;
height: calc(100vh - #{$excluding-size});
@@ -180,6 +195,13 @@
background: $bg-color-light-gray;
}
.menu-item {
&:hover,
&.selected {
background: rgba($color: #000, $alpha: 0.05);
}
}
.table-size {
opacity: 0.4;
}
@@ -200,11 +222,29 @@
.table {
.th {
background: $body-bg;
border-color: rgba($bg-color-light-dark, 0.5);
border-color: lighten($bg-color-light-gray, 2%);
}
.td {
border-color: rgba($bg-color-light-dark, 0.5);
.tr {
background-color: lighten($bg-color-light-gray, 2%);
.td:first-child {
border-left: 2px solid $body-bg;
}
.td {
border-color: $body-bg;
&:focus {
box-shadow: inset 0 0 0 2px lighten($body-font-color, 10%);
background-color: $body-font-color-dark;
}
.editable-field {
box-shadow: inset 0 0 0 2px lighten($body-font-color, 10%);
background-color: $body-font-color-dark;
}
}
}
}
}

View File

@@ -5,12 +5,14 @@ import Vuex from 'vuex';
import application from './modules/application.store';
import settings from './modules/settings.store';
import history from './modules/history.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';
import ipcUpdates from './plugins/ipcUpdates';
import ipcExceptions from './plugins/ipcExceptions';
Vue.use(Vuex);
@@ -19,12 +21,14 @@ export default new Vuex.Store({
modules: {
application,
settings,
history,
scratchpad,
connections,
workspaces,
notifications
},
plugins: [
ipcUpdates
ipcUpdates,
ipcExceptions
]
});

View File

@@ -0,0 +1,54 @@
'use strict';
import Store from 'electron-store';
import { uidGen } from 'common/libs/uidGen';
const persistentStore = new Store({ name: 'history' });
const historySize = 1000;
export default {
namespaced: true,
strict: true,
state: {
history: persistentStore.get('history', {}),
favorites: persistentStore.get('favorites', {})
},
getters: {
getHistoryByWorkspace: state => uid => state.history[uid]
},
mutations: {
SET_HISTORY (state, args) {
if (!(args.uid in state.history))
state.history[args.uid] = [];
state.history[args.uid] = [
{
uid: uidGen('H'),
sql: args.query,
date: new Date(),
schema: args.schema
},
...state.history[args.uid]
];
if (state.history[args.uid].length > historySize)
state.history[args.uid] = state.history[args.uid].slice(0, historySize);
persistentStore.set('history', state.history);
},
DELETE_QUERY_FROM_HISTORY (state, query) {
state.history[query.workspace] = state.history[query.workspace].filter(q => q.uid !== query.uid);
persistentStore.set('history', state.history);
}
},
actions: {
saveHistory ({ commit, getters }, args) {
if (getters.getHistoryByWorkspace(args.uid) &&
getters.getHistoryByWorkspace(args.uid).length &&
getters.getHistoryByWorkspace(args.uid)[0].sql === args.query
) return;
commit('SET_HISTORY', args);
},
deleteQueryFromHistory ({ commit }, query) {
commit('DELETE_QUERY_FROM_HISTORY', query);
}
}
};

View File

@@ -2,6 +2,9 @@
import i18n from '@/i18n';
import Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' });
const isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)');
const defaultAppTheme = isDarkTheme.matches ? 'dark' : 'light';
const defaultEditorTheme = isDarkTheme.matches ? 'twilight' : 'sqlserver';
export default {
namespaced: true,
@@ -14,8 +17,8 @@ export default {
data_tab_limit: persistentStore.get('data_tab_limit', 1000),
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'),
application_theme: persistentStore.get('application_theme', defaultAppTheme),
editor_theme: persistentStore.get('editor_theme', defaultEditorTheme),
editor_font_size: persistentStore.get('editor_font_size', 'medium'),
restore_tabs: persistentStore.get('restore_tabs', true)
},

View File

@@ -35,14 +35,14 @@ export default {
},
getConnected: state => {
return state.workspaces
.filter(workspace => workspace.connection_status === 'connected')
.filter(workspace => workspace.connectionStatus === 'connected')
.map(workspace => workspace.uid);
},
getLoadedSchemas: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).loaded_schemas;
return state.workspaces.find(workspace => workspace.uid === uid).loadedSchemas;
},
getSearchTerm: state => uid => {
return state.workspaces.find(workspace => workspace.uid === uid).search_term;
return state.workspaces.find(workspace => workspace.uid === uid).searchTerm;
}
},
mutations: {
@@ -72,9 +72,9 @@ export default {
indexTypes,
customizations,
structure,
connection_status: 'connected',
connectionStatus: 'connected',
tabs: cachedTabs,
selected_tab: cachedTabs.length ? cachedTabs[0].uid : null,
selectedTab: cachedTabs.length ? cachedTabs[0].uid : null,
version
}
: workspace);
@@ -85,8 +85,8 @@ export default {
...workspace,
structure: {},
breadcrumbs: {},
loaded_schemas: new Set(),
connection_status: 'connecting'
loadedSchemas: new Set(),
connectionStatus: 'connecting'
}
: workspace);
},
@@ -96,8 +96,8 @@ export default {
...workspace,
structure: {},
breadcrumbs: {},
loaded_schemas: new Set(),
connection_status: 'failed'
loadedSchemas: new Set(),
connectionStatus: 'failed'
}
: workspace);
},
@@ -107,8 +107,8 @@ export default {
...workspace,
structure: {},
breadcrumbs: {},
loaded_schemas: new Set(),
connection_status: 'disconnected'
loadedSchemas: new Set(),
connectionStatus: 'disconnected'
}
: workspace);
},
@@ -180,7 +180,7 @@ export default {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? {
...workspace,
search_term: term
searchTerm: term
}
: workspace);
},
@@ -292,7 +292,7 @@ export default {
persistentStore.set(uid, state.workspaces.find(workspace => workspace.uid === uid).tabs);
},
SELECT_TAB (state, { uid, tab }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selected_tab: tab } : workspace);
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selectedTab: tab } : workspace);
},
UPDATE_TABS (state, { uid, tabs }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, tabs } : workspace);
@@ -356,7 +356,28 @@ export default {
ADD_LOADED_SCHEMA (state, payload) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === payload.uid)
workspace.loaded_schemas.add(payload.schema);
workspace.loadedSchemas.add(payload.schema);
return workspace;
});
},
ADD_LOADING_ELEMENT (state, payload) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === payload.uid)
workspace.loadingElements.push(payload.element);
return workspace;
});
},
REMOVE_LOADING_ELEMENT (state, payload) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === payload.uid) {
const loadingElements = workspace.loadingElements.filter(el =>
el.schema !== payload.element.schema &&
el.name !== payload.element.name &&
el.type !== payload.element.type
);
workspace = { ...workspace, loadingElements };
}
return workspace;
});
}
@@ -513,16 +534,17 @@ export default {
addWorkspace ({ commit }, uid) {
const workspace = {
uid,
connection_status: 'disconnected',
selected_tab: 0,
search_term: '',
connectionStatus: 'disconnected',
selectedTab: 0,
searchTerm: '',
tabs: [],
structure: {},
variables: [],
collations: [],
users: [],
breadcrumbs: {},
loaded_schemas: new Set()
loadingElements: [],
loadedSchemas: new Set()
};
commit('ADD_WORKSPACE', workspace);
@@ -545,6 +567,12 @@ export default {
addLoadedSchema ({ commit, getters }, schema) {
commit('ADD_LOADED_SCHEMA', { uid: getters.getSelected, schema });
},
addLoadingElement ({ commit, getters }, element) {
commit('ADD_LOADING_ELEMENT', { uid: getters.getSelected, element });
},
removeLoadingElement ({ commit, getters }, element) {
commit('REMOVE_LOADING_ELEMENT', { uid: getters.getSelected, element });
},
setSearchTerm ({ commit, getters }, term) {
commit('SET_SEARCH_TERM', { uid: getters.getSelected, term });
},
@@ -553,70 +581,25 @@ export default {
const workspaceTabs = state.workspaces.find(workspace => workspace.uid === uid);
switch (type) {
case 'temp-data': {
const existentTab = workspaceTabs
? workspaceTabs.tabs.find(tab =>
tab.schema === schema &&
tab.elementName === elementName &&
tab.elementType === elementType &&
['temp-data', 'data'].includes(tab.type))
: false;
if (existentTab) { // if data tab exists
tabUid = existentTab.uid;
}
else {
const tempTabs = workspaceTabs ? workspaceTabs.tabs.filter(tab => tab.type === 'temp-data') : false;
if (tempTabs && tempTabs.length) { // if temp table already opened
for (const tab of tempTabs) {
commit('REPLACE_TAB', { uid, tab: tab.uid, type, schema, elementName, elementType });
tabUid = tab.uid;
}
}
else {
tabUid = uidGen('T');
commit('NEW_TAB', { uid, tab: tabUid, content, type, autorun, schema, elementName, elementType });
}
}
}
break;
case 'data': {
const existentTab = workspaceTabs
? workspaceTabs.tabs.find(tab =>
tab.schema === schema &&
tab.elementName === elementName &&
tab.elementType === elementType &&
['temp-data', 'data'].includes(tab.type))
: false;
if (existentTab) {
commit('REPLACE_TAB', { uid, tab: existentTab.uid, type, schema, elementName, elementType });
tabUid = existentTab.uid;
}
else {
tabUid = uidGen('T');
commit('NEW_TAB', { uid, tab: tabUid, content, type, autorun, schema, elementName, elementType });
}
}
break;
case 'table-props': {
const existentTab = workspaceTabs
? workspaceTabs.tabs.find(tab =>
tab.elementName === elementName &&
tab.elementType === elementType &&
tab.type === type)
: false;
if (existentTab) {
commit('REPLACE_TAB', { uid, tab: existentTab.uid, type, schema, elementName, elementType });
tabUid = existentTab.uid;
}
else {
tabUid = uidGen('T');
commit('NEW_TAB', { uid, tab: tabUid, content, type, autorun, schema, elementName, elementType });
}
}
case 'new-table':
case 'new-trigger':
case 'new-trigger-function':
case 'new-function':
case 'new-routine':
case 'new-scheduler':
tabUid = uidGen('T');
commit('NEW_TAB', {
uid,
tab: tabUid,
content,
type,
autorun,
schema,
elementName,
elementType
});
break;
case 'temp-data':
case 'temp-trigger-props':
case 'temp-trigger-function-props':
case 'temp-function-props':
@@ -635,6 +618,7 @@ export default {
}
else {
const tempTabs = workspaceTabs ? workspaceTabs.tabs.filter(tab => tab.type.includes('temp-')) : false;
if (tempTabs && tempTabs.length) { // if temp tab already opened
for (const tab of tempTabs) {
if (tab.isChanged) {
@@ -663,6 +647,8 @@ export default {
}
}
break;
case 'data':
case 'table-props':
case 'trigger-props':
case 'trigger-function-props':
case 'function-props':
@@ -697,7 +683,7 @@ export default {
checkSelectedTabExists ({ state, commit }, uid) {
const workspace = state.workspaces.find(workspace => workspace.uid === uid);
const isSelectedExistent = workspace
? workspace.tabs.some(tab => tab.uid === workspace.selected_tab)
? workspace.tabs.some(tab => tab.uid === workspace.selectedTab)
: false;
if (!isSelectedExistent && workspace.tabs.length)

View File

@@ -0,0 +1,7 @@
import { ipcRenderer } from 'electron';
export default store => {
ipcRenderer.on('unhandled-exception', (event, error) => {
store.dispatch('notifications/addNotification', { status: 'error', message: error.message });
});
};